package org.threadly.util.debug;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentSkipListMap;
import org.threadly.util.Clock;
/**
* Often times when trying to understand a concurrency issue, adding logging may solve that
* problem. This class is designed to help work around that problem in some situations. It works
* by not actually outputting the logs collected, but storing them in a concurrent structure.
* <p>
* t will ensure that when your ready to dump all the logs, they will be returned in the order
* they were provided. Since these are not outputted to the actual log stream, make sure any
* logging relevant to the issue is captured by this utility.
* <p>
* This utility has several deficiencies, the largest of which is using {@link System#nanoTime()}
* for log ordering. Since nanosecond time can roll over from positive to negative, in those rare
* situations log ordering may be incorrect. It is design only as a debugging aid and should
* NEVER be included after debugging is completed.
*
* @since 1.0.0
*/
public class DebugLogger {
protected static final boolean LOG_TIME_DEFAULT = false;
protected static final String TIME_DELIM = " - ";
protected static volatile ConcurrentSkipListMap<Long, String> logMap = new ConcurrentSkipListMap<>();
/**
* This adds a log message to the stored log. Keep in mind this will continue to consume more
* and more memory until {@link #getAllStoredMessages()} is called.
*
* @param msg message to be stored into log map
*/
public static void log(String msg) {
long time = Clock.accurateTimeNanos();
String replacement = logMap.putIfAbsent(time, msg);
while (replacement != null) {
replacement = logMap.putIfAbsent(++time, msg);
}
}
/**
* This call checks how many messages are waiting in the stored map. This can be useful if you
* are possibly storing lots of messages and may need to know when to regularly drain the map.
*
* @return current number of stored log messages
*/
public static int getCurrentMessageQty() {
return logMap.size();
}
/**
* Request to get and clear all currently stored log messages. This will return all the log
* messages formatted into a single string, separated by new line characters.
* <p>
* This calls {@link #getAllStoredMessages(boolean)} with a default of NOT including the time in
* nanoseconds.
*
* @return string with all log messages, separated by a new line
*/
public static String getAllStoredMessages() {
return getAllStoredMessages(LOG_TIME_DEFAULT);
}
/**
* Request to get and clear all currently stored log messages. This will return all the log
* messages formatted into a single string, separated by new line characters.
*
* @param includeLogTimes boolean to include time in nanoseconds that log message was recorded
* @return string with all log messages, separated by a new line
*/
public static String getAllStoredMessages(boolean includeLogTimes) {
ConcurrentSkipListMap<Long, String> currentLog = logMap;
logMap = new ConcurrentSkipListMap<>();
try {
Thread.sleep(100); // wait for any possibly log messages attempting to be currently stored
} catch (InterruptedException e) {
// go ahead and produce results now and quit quickly
Thread.currentThread().interrupt();
}
StringBuilder result = new StringBuilder();
Iterator<Entry<Long, String>> it = currentLog.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> entry = it.next();
if (includeLogTimes) {
result.append(entry.getKey())
.append(TIME_DELIM)
.append(entry.getValue());
} else {
result.append(entry.getValue());
}
if (it.hasNext()) {
result.append(System.lineSeparator());
}
}
return result.toString();
}
/**
* This call retrieves and removes the oldest stored log messages. It will only return at most
* the maximum quantity provided, but may return less if not that many messages are currently
* available. This call is slightly less efficient than {@link #getAllStoredMessages()}.
* <p>
* This calls {@link #getOldestLogMessages(int, boolean)} with a default of NOT including the
* time in nanoseconds.
*
* @param qty maximum quantity of messages to retrieve
* @return string with requested log messages, separated by a new line
*/
public static String getOldestLogMessages(int qty) {
return getOldestLogMessages(qty, LOG_TIME_DEFAULT);
}
/**
* This call retrieves and removes the oldest stored log messages. It will only return at most
* the maximum quantity provided, but may return less if not that many messages are currently
* available. This call is slightly less efficient than {@link #getAllStoredMessages()}.
*
* @param qty maximum quantity of messages to retrieve
* @param includeLogTimes boolean to include time in nanoseconds that log message was recorded
* @return string with requested log messages, separated by a new line
*/
public static String getOldestLogMessages(int qty, boolean includeLogTimes) {
int collectedQty = 0;
StringBuilder result = new StringBuilder();
Iterator<Entry<Long, String>> it = logMap.entrySet().iterator();
while (it.hasNext() && collectedQty < qty) {
Entry<Long, String> entry = it.next();
if (includeLogTimes) {
result.append(entry.getKey())
.append(TIME_DELIM)
.append(entry.getValue());
} else {
result.append(entry.getValue());
}
if (it.hasNext()) {
result.append(System.lineSeparator());
}
collectedQty++;
it.remove();
}
return result.toString();
}
}