HistoricalLog.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.arrow.memory.util;

import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.FormatString;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;

/**
 * Utility class that can be used to log activity within a class for later logging and debugging.
 * Supports recording events and recording the stack at the time they occur.
 */
public class HistoricalLog {

  private final Deque<Event> history = new ArrayDeque<>();
  private final String idString; // the formatted id string
  private final int limit; // the limit on the number of events kept
  private @Nullable Event firstEvent; // the first stack trace recorded

  /**
   * Constructor. The format string will be formatted and have its arguments substituted at the time
   * this is called.
   *
   * @param idStringFormat {@link String#format} format string that can be used to identify this
   *     object in a log. Including some kind of unique identifier that can be associated with the
   *     object instance is best.
   * @param args for the format string, or nothing if none are required
   */
  @FormatMethod
  public HistoricalLog(@FormatString final String idStringFormat, Object... args) {
    this(Integer.MAX_VALUE, idStringFormat, args);
  }

  /**
   * Constructor. The format string will be formatted and have its arguments substituted at the time
   * this is called.
   *
   * <p>This form supports the specification of a limit that will limit the number of historical
   * entries kept (which keeps down the amount of memory used). With the limit, the first entry made
   * is always kept (under the assumption that this is the creation site of the object, which is
   * usually interesting), and then up to the limit number of entries are kept after that. Each time
   * a new entry is made, the oldest that is not the first is dropped.
   *
   * @param limit the maximum number of historical entries that will be kept, not including the
   *     first entry made
   * @param idStringFormat {@link String#format} format string that can be used to identify this
   *     object in a log. Including some kind of unique identifier that can be associated with the
   *     object instance is best.
   * @param args for the format string, or nothing if none are required
   */
  @FormatMethod
  public HistoricalLog(final int limit, @FormatString final String idStringFormat, Object... args) {
    this.limit = limit;
    this.idString = String.format(idStringFormat, args);
    this.firstEvent = null;
  }

  /**
   * Record an event. Automatically captures the stack trace at the time this is called. The format
   * string will be formatted and have its arguments substituted at the time this is called.
   *
   * @param noteFormat {@link String#format} format string that describes the event
   * @param args for the format string, or nothing if none are required
   */
  @FormatMethod
  public synchronized void recordEvent(@FormatString final String noteFormat, Object... args) {
    final String note = String.format(noteFormat, args);
    final Event event = new Event(note);
    if (firstEvent == null) {
      firstEvent = event;
    }
    if (history.size() == limit) {
      history.removeFirst();
    }
    history.add(event);
  }

  /**
   * Write the history of this object to the given {@link StringBuilder}. The history includes the
   * identifying string provided at construction time, and all the recorded events with their stack
   * traces.
   *
   * @param sb {@link StringBuilder} to write to
   * @param includeStackTrace whether to include the stacktrace of each event in the history
   */
  public void buildHistory(final StringBuilder sb, boolean includeStackTrace) {
    buildHistory(sb, 0, includeStackTrace);
  }

  /**
   * Build the history and write it to sb.
   *
   * @param sb output
   * @param indent starting indent (usually "")
   * @param includeStackTrace whether to include the stacktrace of each event.
   */
  public synchronized void buildHistory(
      final StringBuilder sb, int indent, boolean includeStackTrace) {
    final char[] indentation = new char[indent];
    final char[] innerIndentation = new char[indent + 2];
    Arrays.fill(indentation, ' ');
    Arrays.fill(innerIndentation, ' ');

    sb.append(indentation).append("event log for: ").append(idString).append('\n');

    if (firstEvent != null) {
      long time = firstEvent.time;
      String note = firstEvent.note;
      final StackTrace stackTrace = firstEvent.stackTrace;
      sb.append(innerIndentation).append(time).append(' ').append(note).append('\n');
      if (includeStackTrace) {
        stackTrace.writeToBuilder(sb, indent + 2);
      }

      for (final Event event : history) {
        if (event == firstEvent) {
          continue;
        }
        sb.append(innerIndentation)
            .append("  ")
            .append(event.time)
            .append(' ')
            .append(event.note)
            .append('\n');

        if (includeStackTrace) {
          event.stackTrace.writeToBuilder(sb, indent + 2);
          sb.append('\n');
        }
      }
    }
  }

  /**
   * Write the history of this object to the given {@link Logger}. The history includes the
   * identifying string provided at construction time, and all the recorded events with their stack
   * traces.
   *
   * @param logger {@link Logger} to write to
   */
  public void logHistory(final Logger logger) {
    final StringBuilder sb = new StringBuilder();
    buildHistory(sb, 0, true);
    logger.debug(sb.toString());
  }

  private static class Event {

    private final String note; // the event text
    private final StackTrace stackTrace; // where the event occurred
    private final long time;

    public Event(final String note) {
      this.note = note;
      this.time = System.nanoTime();
      stackTrace = new StackTrace();
    }
  }
}