Back-End

7 mai, 2014

Rastreamento de exceções em aplicativos com o Spring: padrão de busca – Parte 02

Publicidade

No meu último artigo, comecei a falar sobre a necessidade de descobrir se seu aplicativo está ou não se comportando mal no ambiente de produção. Eu disse que um método de monitorar a sua aplicação é verificar seus arquivos de log por exceções e tomar medidas apropriadas se uma for encontrada. Obviamente, os arquivos de log podem levar até centenas de megabytes de espaço em disco e é impraticável e muito chato monitorá-los manualmente.

Eu também disse que havia várias maneiras de monitorar automaticamente os arquivos de log e propus um utilitário baseado em Spring que varre arquivos de log diariamente e lhe envia um e-mail, caso/quando encontrar qualquer exceção.

Eu só cheguei a descrever a primeira classe: o FileLocator, que procurará um diretório e seus subdiretórios por arquivos de log. Quando encontra um, ele passa para o FileValidator.

O FileValidator tem que executar várias verificações no arquivo. Em primeiro lugar, tem que determinar se o arquivo é novo o suficiente para examinar as exceções. A ideia é que, como o aplicativo é executado periodicamente, não há nenhuma razão para procurar por erros em todos os arquivos encontrados no diretório; queremos apenas os arquivos que foram criados ou atualizados desde a última execução do aplicativo.

A ideia por trás desse projeto é combinar várias implementações da mesma interface, criando um objeto agregador que é responsável por validar os arquivos. O leitor perspicaz vai perceber que essa é uma implementação do design pattern Delegate.

spring-1

No diagrama de classe acima, instâncias de RegexValidator e FileAgeValidator são injetadas no FileValidator, que delega as tarefas de validação para essas classes.

Tomando cada um deles por vez e lidando com a interface Validator primeiro…

public interface Validator {

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

}

O código acima demonstra a simplicidade da interface Validator. Ela tem um único método validate (obj T), que é uma chamada de método genérico que aumenta a flexibilidade e a possibilidade de reutilização da interface. Quando classes implementam essa interface, elas podem alterar o tipo de argumento de entrada para atender a seus próprios propósitos… conforme demonstrado pela primeira implementação abaixo:

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;
  }
}

A classe RegexValidator tem um único argumento construtor que recebe uma string de expressão regular. Isso é, então, convertido para uma instância da variável Pattern, que é então utilizada pelo método validate (string T) para testar se o argumento de entrada String coincide ou não com a expressão regular do construtor original. Se isso acontecer, então validate (string T) retornará 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;
  }

}

A segunda implementação do Validator (obj T) é a FileAgeValidator mostrada acima, e a primeira coisa a se notar é que tudo é impulsionado pela propriedade max.days. Esta é injetada para dentro da notação @Value do FileAgeValidator determinado pela instância da variável maxDays. Essa variável determina a idade máxima do arquivo em dias. Se o arquivo for mais antigo do que esse valor, então o validate (obj T) retornará false.

Nesta implementação, o validate (T obj) argumento ‘obj’ é enviado ao objeto File, que é então utilizado para converter a data do arquivo em um objeto Calendar. A próxima linha de código converte a variável maxDays em um segundo objeto Calendar: ageLimit. O ageLimit é então comparado com o objeto fileDate. Se o fileDate é posterior ao ageLimit, então validate (T obj)  retornará true.

A classe final do pacote validator é o FileValidator, que, como mostrado acima, delega um monte de sua responsabilidade para os três outros validadores agregados: um FileAgeValidator e dois RegexValidator.

@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;
      }
    }
  }

}

O validate (T obj) do FileValidator recebe um File como um argumento. Sua primeira responsabilidade é a de validar a idade do arquivo. Se esse validador retornar true, então ele informa a instância da classe Report que encontrou um novo arquivo válido. Em seguida, ele verifica o arquivo à procura de erros, acrescentando aqueles que encontra à instância do Report. Ele faz isso usando um BufferedReader para verificar cada linha do arquivo por vez. Antes de verificar se uma linha contém um erro, ele checa se ela não está excluída da verificação – ou seja, que não coincide com as exceções excluídas ou aquelas em que não estamos interessados. Se a linha não coincidir com as exceções, então é feita a verificação por erros que precisamos encontrar usando a segunda instância de RegexValidator. Se a linha contém um erro, ele é adicionado a um objeto List<String>. Algumas linhas seguintes são, então, lidas do arquivo adicionado à lista para tornar o relatório mais legível.

E assim o parser do arquivo continua, verificando cada linha de cada vez em busca de erros e construindo um relatório, que pode ser processado posteriormente.

Isso cobre a validação de arquivos usando o design pattern Delegate acrescentando quaisquer exceções encontradas ao Report, mas como é que esse objeto Report funciona? Eu não mencionei isso, e como é gerada a saída? Mais sobre isso na próxima oportunidade.

O código para este artigo está disponível no GitHub: https://github.com/roghughe/captaindebug/tree/master/error-track

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2014/03/tracking-exceptions-with-spring-part-2.html#.U06lYqYe939