Java tutorial
/* * 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; } }