Package com.greplin.gec

Source Code of com.greplin.gec.GecLogbackAppender

/*
* 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 ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.CoreConstants;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;

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 GecLogbackAppender extends AppenderBase<ILoggingEvent> {
  /**
   * 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<String> passthroughExceptions;

  /**
   * Creates a new appender.
   */
  public GecLogbackAppender() {
    // setThreshold(Level.ERROR); // FIXME: replaced by filters ?
    this.passthroughExceptions = new HashSet<String>();
    this.passthroughExceptions.add(
        InvocationTargetException.class.getCanonicalName());
  }

  @Override
  public void start() {
    /* FIXME outdated docs ?
    if (this.layout == null) {
      addError("No layout set for the appender named [" + name + "].");
      return;
    }
    */
    super.start();
  }

  @Override
  protected void append(final ILoggingEvent loggingEvent) {

    try {
      if (loggingEvent.getThrowableProxy() == null
          && loggingEvent.getLevel().toInt() < Level.ERROR_INT) {
        // 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.getThrowableProxy() == null) {
        writeFormattedException(
            loggingEvent.getMessage(),
            loggingEvent.getLevel(),
            writer);
      } else {
        writeFormattedException(
            loggingEvent.getMessage(),
            loggingEvent.getThrowableProxy(),
            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();
    }
  }

  /**
   * 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 = GecLogbackAppender.getStackTrace(new Throwable());
    String[] lines = backtrace.split("\n");
    StringBuilder builder = new StringBuilder();
    for (String line : lines) {
      if (!line.contains("com.greplin.gec.GecLogbackAppender.")) {
        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 {
    this.writeFormattedException(message,
        new ThrowableProxy(throwable), level, out);
  }

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

    IThrowableProxy rootThrowable = throwableProxy;
    while (this.passthroughExceptions.contains(rootThrowable.getClassName())
        && rootThrowable.getCause() != null) {
      rootThrowable = rootThrowable.getCause();
    }

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


  /**
   * Renders a stacktrace.
   * @param throwableProxy an IThrowableProxy
   * @return a string rendering of the stack trace
   */
  protected static String getStackTrace(final IThrowableProxy throwableProxy) {
    StringBuilder builder = new StringBuilder();
    for (StackTraceElementProxy step
        : throwableProxy.getStackTraceElementProxyArray()) {
      String string = step.toString();
      builder.append(CoreConstants.TAB).append(string);
      ThrowableProxyUtil.subjoinPackagingData(builder, step);
      builder.append(CoreConstants.LINE_SEPARATOR);
    }
    return builder.toString();
  }

  /**
   * Renders a stacktrace.
   * @param t a throwable
   * @return a string rendering of the stack trace
   */
  protected static String getStackTrace(final Throwable t) {
    return GecLogbackAppender.getStackTrace(new ThrowableProxy(t));
  }

  /**
   * 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.getCanonicalName());
  }


  /**
   * 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.GecLogbackAppender

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.