Tuesday, 11 March 2014

Tracking Exceptions With Spring - Part 2 - Delegate Pattern

In my last blog, I started to talk about the need to figure out whether or not your application is misbehaving in it's production environment. I said that one method of monitoring your application is by checking its log files for exceptions and taking appropriate action if one is found. Obviously, log files can take up hundreds of megabytes of disk space and it's impractical and really boring to monitor them by hand.

I also said that there were several ways of automatically monitoring log files and proposed a Spring based utility that combs log files daily and sends you an email if / when it finds any exceptions.

I only got as far as describing the first class: the FileLocator, which will search a directory and it's sub-directories for log files. When it finds one, it passes it to the FileValidator.

The FileValidator has to perform several checks on the file. Firstly, it has to determine whether the file is young enough to examine for exceptions. The idea is that as the application runs periodically, there's no point in checking all the files found in the directory for errors, we only want those files that have been created or updated since the application last ran.

The idea behind this design is to combine several implementations of the same interface, creating an aggregate object that’s responsible for validating files. The eagle-eyed reader will notice that this is an implementation of the Delegate Pattern.


In the class diagram above instances of RegexValidator and FileAgeValidator are injected into the FileValidator and it delegates its validation tasks to these classes.

Taking each of these in turn and dealing with the Validator interface first…

public interface Validator {

 
/** The validation method */
 
public <T> boolean validate(T obj);

}

The code above demonstrates the simplicity of the Validator interface. It has a single method validate(T obj), which is a Generic Method call that increases the flexibility and re-usability of this interface. When classes implement this interface, they can change the input argument type to suit their own purposes… as demonstrated by the first implementation below:

public class RegexValidator implements Validator {

 
private static final Logger logger = LoggerFactory.getLogger(RegexValidator.class);

 
private final Pattern pattern;

 
public RegexValidator(String regex) {
   
pattern = Pattern.compile(regex);
    logger.info
("loaded regex: {}", regex);
 
}

 
@Override
 
public <T> boolean validate(T string) {

   
boolean retVal = false;
    Matcher matcher = pattern.matcher
((String) string);
    retVal = matcher.matches
();
   
if (retVal && logger.isDebugEnabled()) {
     
logger.debug("Found error line: {}", string);
   
}

   
return retVal;
 
}
}

The RegexValidator class has a single argument constructor that takes a Regular Expression string. This is then converted to a Pattern instance variable and is used by the validate(T string) method to test whether or not the String input argument matches original constructor arg regular expression. If it does, then validate(T string) will return true.

@Service
public class FileAgeValidator implements Validator {

 
@Value("${max.days}")
 
private int maxDays;

 
/**
   * Validate the age of the file.
   *
   *
@see com.captaindebug.errortrack.Validator#validate(java.lang.Object)
   */
 
@Override
 
public <T> boolean validate(T obj) {

   
File file = (File) obj;
    Calendar fileDate = getFileDate
(file);

    Calendar ageLimit = getFileAgeLimit
();

   
boolean retVal = false;
   
if (fileDate.after(ageLimit)) {
     
retVal = true;
   
}

   
return retVal;
 
}

 
private Calendar getFileAgeLimit() {

   
Calendar cal = Calendar.getInstance();
    cal.add
(Calendar.DAY_OF_MONTH, -1 * maxDays);
   
return cal;
 
}

 
private Calendar getFileDate(File file) {

   
long fileDate = file.lastModified();
    Calendar when = Calendar.getInstance
();
    when.setTimeInMillis
(fileDate);
   
return when;
 
}

}

The second Validator(T obj) implementation is the FileAgeValidator shown above and the first thing to note is that the whole thing is driven by the max.days property. This is injected into the FileAgeValidator’s @Value annotated maxDays instance variable. This variable determines the maximum age of the file in days. This the file is older than this value, then the validate(T obj) will return false.

In this implementation, the validate(T obj) ‘obj’ argument is cast to a File object, which is then used to convert the date of the file into a Calendar object. The next line of code converts the maxDays variable into a second Calendar object: ageLimit. The ageLimit is then compared with the fileDate object. If the fileDate is after the ageLimit then validate(T obj) returns true.

The final class in the validator package is the FileValidator, which as shown above delegates a lot of its responsibility to the other three other aggregated validators: one FileAgeValidator and two RegexValidator’s.

@Service
public class FileValidator implements Validator {

 
private static final Logger logger = LoggerFactory.getLogger(FileValidator.class);

 
@Value("${following.lines}")
 
private Integer extraLineCount;

 
@Autowired
  @Qualifier
("scan-for")
 
private RegexValidator scanForValidator;

 
@Autowired(required = false)
 
@Qualifier("exclude")
 
private RegexValidator excludeValidator;

 
@Autowired
 
private FileAgeValidator fileAgeValidator;

 
@Autowired
 
private Results results;

 
@Override
 
public <T> boolean validate(T obj) {

   
boolean retVal = false;
    File file =
(File) obj;
   
if (fileAgeValidator.validate(file)) {
     
results.addFile(file.getPath());
      checkFile
(file);
      retVal =
true;
   
}
   
return retVal;
 
}

 
private void checkFile(File file) {

   
try {
     
BufferedReader in = createBufferedReader(file);
      readLines
(in, file);
      in.close
();
   
} catch (Exception e) {
     
logger.error("Error whilst processing file: " + file.getPath() + " Message: " + e.getMessage(), e);
   
}
  }

 
@VisibleForTesting
 
BufferedReader createBufferedReader(File file) throws FileNotFoundException {
   
BufferedReader in = new BufferedReader(new FileReader(file));
   
return in;
 
}

 
private void readLines(BufferedReader in, File file) throws IOException {
   
int lineNumber = 0;
    String line;
   
do {
     
line = in.readLine();
     
if (isNotNull(line)) {
       
processLine(line, file.getPath(), ++lineNumber, in);
     
}
    }
while (isNotNull(line));
 
}

 
private boolean isNotNull(Object obj) {
   
return obj != null;
 
}

 
private int processLine(String line, String filePath, int lineNumber, BufferedReader in) throws IOException {

   
if (canValidateLine(line) && scanForValidator.validate(line)) {
     
List<String> lines = new ArrayList<String>();
      lines.add
(line);
      addExtraDetailLines
(in, lines);
      results.addResult
(filePath, lineNumber, lines);
      lineNumber += extraLineCount;
   
}

   
return lineNumber;
 
}

 
private boolean canValidateLine(String line) {
   
boolean retVal = true;
   
if (isNotNull(excludeValidator)) {
     
retVal = !excludeValidator.validate(line);
   
}
   
return retVal;
 
}

 
private void addExtraDetailLines(BufferedReader in, List<String> lines) throws IOException {

   
for (int i = 0; i < extraLineCount; i++) {
     
String line = in.readLine();
     
if (isNotNull(line)) {
       
lines.add(line);
     
} else {
       
break;
     
}
    }
  }

}

The FileValidator’s validate(T obj) takes a File as an argument. Its first responsibility is to validate the age of the file. If that validator returns true, then it informs the Report class that it’s found a new, valid file. It then checks the file for errors, adding any it finds to the Report instance. It does this by using a BufferedReader to check each line of the file in turn. Before checking whether a line contains an error, it checks that the line isn’t excluded from the check - i.e. that it doesn’t match the excluded exceptions or ones we're not interested in. If the line doesn’t match the excluded exceptions, then it’s checked for exceptions that we need to find using the second instance of the RegexValidator. If the line does contain an error it’s added to a List<String> object. A number of following lines are then read from the file added to the list to make the report more readable.

And so, the file parsing continues, checking each line at a time looking for errors and building up a report, which can be processed later.

That cover’s validating files using Delegate Pattern adding any exceptions found to the Report, but how does this Report object work? I've not mentioned it, and how is the output generated? More on that next time.


The code for this blog is available on Github at: https://github.com/roghughe/captaindebug/tree/master/error-track.

1 comment:

Yogi said...

Although I didnot execute and check this code, how fine it works, but i really really like the idea of monitoring the log file and sending emails only any errors that occurs.

Really very well thought... I would definitely try to implement this idea in my future projects where ever possible...

I believe, making it a s simple jar file/plug and play utility to any project will help more...

Like, if it is possible that I put the jar and it starts scanning *.log files in the current directory. And I should not be needed to make any configurations, would make it more simple and easy to use.