Package com.greplin.gec

Source Code of com.greplin.gec.GecLog4jAppender

/*
* Copyright 2011 The greplin-exception-catcher Authors.
*
* 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.greplin.gec;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.spi.LoggingEvent;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;


/**
* log4j appender that writes exceptions to a file to be picked up by upload.py.
*/
public final class GecLog4jAppender extends AppenderSkeleton {
  /**
   * Number of prefixes to use to reduce risk of server invocations overwriting
   * each other's error logs.
   */
  private static final int MAX_BASENAME = 10;

  /**
   * Maximum number of errors a single instance of the server can write.
   */
  private static final int MAX_ERRORS = 10000;

  /**
   * Base name for error files.  Used to reduce risk of server invocations
   * clobbering each other's error logs.
   */
  private static final String BASENAME;

  /**
   * ID for the next error will be this value modulo MAX_ERRORS.
   */
  private static final AtomicLong ERROR_ID;

  static {
    Random random = new Random();
    // We randomly choose a base name and starting number to minimize the risk
    // of multiple invocations of a server overwriting error logs.
    BASENAME = random.nextInt(MAX_BASENAME) + "-";
    ERROR_ID = new AtomicLong(random.nextInt(MAX_ERRORS));
  }

  /**
   * Name of the project we are logging exceptions for.
   */
  private String project;

  /**
   * Name of the environment (prod/devel/etc.) we are logging exceptions in.
   */
  private String environment;

  /**
   * The name of this server.
   */
  private String serverName;

  /**
   * The directory to write exception files.
   */
  private String outputDirectory;


  /**
   * Set of classes that only exist to contain exceptions.
   */
  private final Set<Class<? extends Throwable>> passthroughExceptions;

  /**
   * Creates a new appender.
   */
  public GecLog4jAppender() {
    setThreshold(Level.ERROR);
    this.passthroughExceptions = new HashSet<Class<? extends Throwable>>();
    this.passthroughExceptions.add(InvocationTargetException.class);
  }

  @Override
  protected void append(final LoggingEvent loggingEvent) {
    try {
      if (loggingEvent.getThrowableInformation() == null
          && loggingEvent.getLevel().toInt() < this.threshold.toInt()) {
        // Ignore non-exceptions below our threshold.
        return;
      }

      String errorId = BASENAME + (ERROR_ID.incrementAndGet() % MAX_ERRORS);
      String filename = errorId + ".gec.json";
      File output = new File(this.outputDirectory, filename + ".writing");
      Writer writer = new FileWriter(output);

      if (loggingEvent.getThrowableInformation() == null) {
        writeFormattedException(
            loggingEvent.getRenderedMessage(),
            loggingEvent.getLevel(),
            writer);
      } else {
        Throwable throwable =
            loggingEvent.getThrowableInformation().getThrowable();
        writeFormattedException(
            loggingEvent.getRenderedMessage(),
            throwable,
            loggingEvent.getLevel(),
            writer);
      }

      writer.close();

      if (!output.renameTo(new File(this.outputDirectory, filename))) {
        System.err.println("Could not rename to " + filename);
      }
    } catch (IOException e) {
      System.err.println("GEC failed to append: " + e.getMessage());
      e.printStackTrace();
    }
  }

  @Override
  public void close() {
  }

  @Override
  public boolean requiresLayout() {
    return false;
  }

  /**
   * Writes the current context to the given JsonGenerator.
   * @param generator where to write the context
   * @throws IOException if there are IO errors in the destination
   */
  private void writeContext(final JsonGenerator  generator) throws IOException {
    Map<String, String> context = GecContext.get();
    if (!context.isEmpty()) {
      generator.writeFieldName("context");
      generator.writeStartObject();
      for (Map.Entry<String, String> entry : context.entrySet()) {
        generator.writeStringField(entry.getKey(), entry.getValue());
      }
      generator.writeEndObject();
    }
  }

  /**
   * Writes a formatted msg for errors that don't have exceptions.
   *
   * @param message the log message
   * @param level   the error level
   * @param out     the destination
   * @throws IOException if there are IO errors in the destination
   */
  void writeFormattedException(final String message,
                               final Level level,
                               final Writer out)
      throws IOException {
    JsonGenerator generator = new JsonFactory().createJsonGenerator(out);

    String backtrace = ExceptionUtils.getFullStackTrace(new Exception());
    String[] lines = backtrace.split("\n");
    StringBuilder builder = new StringBuilder();
    for (String line : lines) {
      if (!line.contains("com.greplin.gec.GecLog4jAppender.")) {
        builder.append(line);
        builder.append("\n");
      }
    }
    backtrace = builder.toString();

    generator.writeStartObject();
    generator.writeStringField("project", this.project);
    generator.writeStringField("environment", this.environment);
    generator.writeStringField("serverName", this.serverName);
    generator.writeStringField("backtrace", backtrace);
    generator.writeStringField("message", message);
    generator.writeStringField("logMessage", message);
    generator.writeStringField("type", "N/A");
    if (level != Level.ERROR) {
      generator.writeStringField("errorLevel", level.toString());
    }
    writeContext(generator);
    generator.writeEndObject();
    generator.close();
  }

  /**
   * Writes a formatted exception to the given writer.
   *
   * @param message   the log message
   * @param throwable the exception
   * @param level     the error level
   * @param out       the destination
   * @throws IOException if there are IO errors in the destination
   */
  void writeFormattedException(final String message,
                               final Throwable throwable,
                               final Level level,
                               final Writer out)
      throws IOException {
    JsonGenerator generator = new JsonFactory().createJsonGenerator(out);

    Throwable rootThrowable = throwable;
    while (this.passthroughExceptions.contains(rootThrowable.getClass())
        && rootThrowable.getCause() != null) {
      rootThrowable = rootThrowable.getCause();
    }

    generator.writeStartObject();
    generator.writeStringField("project", this.project);
    generator.writeStringField("environment", this.environment);
    generator.writeStringField("serverName", this.serverName);
    generator.writeStringField("backtrace",
        ExceptionUtils.getStackTrace(throwable));
    generator.writeStringField("message", rootThrowable.getMessage());
    generator.writeStringField("logMessage", message);
    generator.writeStringField("type", rootThrowable.getClass().getName());
    if (level != Level.ERROR) {
      generator.writeStringField("errorLevel", level.toString());
    }
    writeContext(generator);
    generator.writeEndObject();
    generator.close();
  }

  /**
   * Sets the environment.
   *
   * @param environment the new environment
   */
  public void setEnvironment(final String environment) {
    this.environment = environment;
  }

  /**
   * Sets the project.
   *
   * @param project the new project
   */
  public void setProject(final String project) {
    this.project = project;
  }

  /**
   * Sets the server name.
   *
   * @param serverName the new server name
   */
  public void setServerName(final String serverName) {
    this.serverName = serverName;
  }

  /**
   * Sets the output directory.
   *
   * @param outputDirectory the new output directory
   */
  public void setOutputDirectory(final String outputDirectory) {
    this.outputDirectory = outputDirectory;
  }

  /**
   * Adds a class that can be considered a container of exceptions only.
   *
   * @param exceptionClass the exception class
   */
  public void addPassthroughExceptionClass(
      final Class<? extends Throwable> exceptionClass) {
    this.passthroughExceptions.add(exceptionClass);
  }


  /**
   * Adds a class that can be considered a container of exceptions only.
   * Adds by name, but does not throw if the class is not found.
   *
   * @param name the exception class
   * @return true if the class exists and was added, false otherwise
   */
  @SuppressWarnings("unchecked")
  public boolean addPassthroughExceptionClass(final String name) {
    try {
      addPassthroughExceptionClass(
          (Class<? extends Throwable>) Class.forName(name));
    } catch (ClassNotFoundException ex) {
      return false;
    }
    return true;
  }

}
TOP

Related Classes of com.greplin.gec.GecLog4jAppender

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.