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

@@ -4163,32 +4163,245 @@
 cktrace(trace);%0A
+%0A        // Can cause NPE and crash HI%0A        //exceptionDataBuilder.setLogging(record.getMessage());%0A%0A        if (record.getMessage() == null) %7B%0A          exceptionDataBuilder.setLogging(%22%22);%0A        %7D else %7B%0A  
         exceptio
@@ -4442,24 +4442,34 @@
 Message());%0A
+        %7D%0A
       %7D%0A    

Fix NPE when logging exceptions that have no message using SLF4J (#2161)