// Copyright 2016 Twitter. All rights reserved. // // 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.twitter.heron.common.utils.logging; import java.io.PrintWriter; import java.io.StringWriter; import java.time.Duration; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.logging.Handler; import java.util.logging.LogRecord; import com.twitter.heron.api.metric.ConcurrentCountMetric; import com.twitter.heron.api.metric.IMetric; import com.twitter.heron.common.utils.metrics.MetricsCollector; import com.twitter.heron.proto.system.Metrics; /** * JUL logging handler which report any log message associated with a Throwable object to * persistent storage. Since error volume can be immense in worst case (e.g throwing exception in a * tight loop) we dedupe the trace based on first element in the throwable trace. */ public class ErrorReportLoggingHandler extends Handler { public static final String NO_TRACE = "No Trace"; private static volatile boolean initialized = false; private static volatile int exceptionsLimit = Integer.MAX_VALUE; private static volatile ConcurrentCountMetric droppedExceptionsCount = new ConcurrentCountMetric(); public ErrorReportLoggingHandler() { super(); } public static String getExceptionLocation(String trace) { if (trace == null) { return NO_TRACE; } String[] firstLine = trace.split("\n"); // Try to get first 2 line of exception and that will define the location of exception. // e.g if the exception trace is : // Exception in thread "main" java.lang.RuntimeException // at com.twitter.myproject.Foo.myfunc(Foo.java:420) // at ... // The First 2 line will be used as location. // TODO: Decide if exception message (first line) should be part of location. if (firstLine.length == 0) { return NO_TRACE; } else if (firstLine.length == 1) { return firstLine[0]; } else { return firstLine[0] + "\n" + firstLine[1]; } } public static synchronized void init(MetricsCollector collector, Duration interval, int maxExceptions) { if (!initialized) { collector.registerMetric( "exception_info", ExceptionRepositoryAsMetrics.INSTANCE, (int) interval.getSeconds()); collector.registerMetric( "dropped_exceptions_count", droppedExceptionsCount, (int) interval.getSeconds()); exceptionsLimit = maxExceptions; } initialized = true; } // All the throwables are logged to in memory ExceptionRepositoryAsMetrics store. This metrics // will flush the exception to metrics manager during getValueAndReset call. @Override public void publish(LogRecord record) { // Convert Log Throwable throwable = record.getThrown(); if (throwable != null) { synchronized (ExceptionRepositoryAsMetrics.INSTANCE) { // We would not include the message if already exceeded the exceptions limit if (ExceptionRepositoryAsMetrics.INSTANCE.getExceptionsCount() >= exceptionsLimit) { droppedExceptionsCount.incr(); return; } // Convert the record StringWriter sink = new StringWriter(); throwable.printStackTrace(new PrintWriter(sink, true)); String trace = sink.toString(); Metrics.ExceptionData.Builder exceptionDataBuilder = ExceptionRepositoryAsMetrics.INSTANCE.getExceptionInfo(trace); exceptionDataBuilder.setCount(exceptionDataBuilder.getCount() + 1); exceptionDataBuilder.setLasttime(new Date().toString()); exceptionDataBuilder.setStacktrace(trace); exceptionDataBuilder.setLogging(record.getMessage()); } } } @Override public void close() { flush(); } @Override public void flush() { // Call getValueAndReset and hope that makes it to metrics manager. Also log to stdout. // Logging to stdout is much more likely to succeed it case of apocalyptic shutdown. System.out.print(ExceptionRepositoryAsMetrics.INSTANCE.getValue().toString()); } // Exception will be stored in this Metrics. Use of metrics simplify exporting the error to // metrics manager. public enum ExceptionRepositoryAsMetrics implements IMetric<Collection<Metrics.ExceptionData.Builder>> { INSTANCE; private HashMap<String, Metrics.ExceptionData.Builder> exceptionStore; ExceptionRepositoryAsMetrics() { exceptionStore = new HashMap<String, Metrics.ExceptionData.Builder>(); } @Override public Collection<Metrics.ExceptionData.Builder> getValueAndReset() { synchronized (ExceptionRepositoryAsMetrics.INSTANCE) { Collection<Metrics.ExceptionData.Builder> metricsValue = exceptionStore.values(); exceptionStore = new HashMap<String, Metrics.ExceptionData.Builder>(); return metricsValue; } } protected int getExceptionsCount() { return exceptionStore.size(); } // Get the underneath exception info without reset // It could be used when we just want to check or query the content public Object getValue() { synchronized (ExceptionRepositoryAsMetrics.INSTANCE) { return exceptionStore.values(); } } // Returns ExceptionData.Builder object for the trace. protected Metrics.ExceptionData.Builder getExceptionInfo(String trace) { Metrics.ExceptionData.Builder exceptionDataBuilder = exceptionStore.get(getExceptionLocation(trace)); if (exceptionDataBuilder == null) { exceptionDataBuilder = Metrics.ExceptionData.newBuilder(); exceptionDataBuilder.setFirsttime(new Date().toString()); exceptionDataBuilder.setCount(0); exceptionDataBuilder.setStacktrace(NO_TRACE); exceptionStore.put(getExceptionLocation(trace), exceptionDataBuilder); } return exceptionDataBuilder; } } }