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.