
com.almworks.jira.structure.util.SyncLogger Maven / Gradle / Ivy
Show all versions of structure-api Show documentation
package com.almworks.jira.structure.util;
import com.almworks.integers.LongIterable;
import com.almworks.integers.LongIterator;
import com.almworks.jira.structure.api.StructureAuth;
import com.almworks.jira.structure.api.StructureException;
import com.almworks.jira.structure.api2g.forest.Forest;
import com.almworks.jira.structure.api2g.forest.ItemForest;
import com.almworks.jira.structure.api2g.item.CoreIdentities;
import com.almworks.jira.structure.api2g.item.ItemIdentity;
import com.almworks.jira.structure.api2g.row.RowManager;
import com.almworks.jira.structure.api2g.structure.StructureManager;
import com.almworks.jira.structure.api2g.sync.StructureSynchronizer;
import com.almworks.jira.structure.api2g.sync.SyncInstance;
import com.almworks.jira.structure.api2g.v2.MissingRowException;
import com.atlassian.annotations.Internal;
import com.atlassian.jira.issue.Issue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import javax.annotation.concurrent.NotThreadSafe;
import java.util.*;
import static com.google.common.collect.Iterators.*;
/**
* This is a utility class to log messages from {@link com.almworks.jira.structure.api2g.sync.StructureSynchronizer synchronizer implementations}.
* It prepends the specified messages with the following information:
*
* - name of the synchronizer taken from its configuration in the set up in the <structure-synchronizer> module (see {@link com.almworks.jira.structure.api2g.sync.StructureSynchronizerModuleDescriptor#getLabel()},
* - mode of synchronization (one-time synchronization, periodical autosync, or resync),
* - name and ID of the structure being synchronized,
* - ID of the {@link SyncInstance synchronizer instance}.
*
*
* Name of the user under which the synchronization is run is not provided, as it is automatically inserted into the log message by JIRA.
*
* Features
* There is a bunch of helper methods to pretty-print the user under which the synchronizer runs ({@link #username()}),
* produce warnings about typical {@link StructureException StructureExceptions} that may be encountered by synchronizers ({@link #warnStructureException(com.almworks.jira.structure.api.StructureException)}),
* and select messages based on the synchronization mode ({@link #selectBySyncMode(Object, Object, Object)}).
*
*
* All logging methods that take {@code Object...} vararg parameter delimit its contents putting a single space before the parameter. If the parameter is a character or a single-character String
* that is a punctuation mark, the space is not inserted before the parameter. If the parameter is an {@code Object[]}, its contents are added to the output as if they belonged to the original {@code Object...} parameter.
*
*
* Example:
* {@code SyncLogger log = ... ; log.warn("cannot run because of", isKaboozle() ? "kaboozle" : new Object[]{"grumbles with error", getError()}, ':', getCause())}
* will produce the following log message:
* {@code autosync #239 for structure 'Unresolved in 2.0' (#156) cannot run because of grumbles with error FATAL: something went wrong}
*
* Methods that end with {@code exceptionIfDebug} attempt to log exception stack trace if log level is DEBUG or lower and print only exception message if log level is higher.
*
* @see StructureSynchronizer
* @since 7.2.0 (Structure 2.0)
*/
@NotThreadSafe
public class SyncLogger {
private static final ThreadLocal SYNC_LOGGER_THREAD_LOCAL = new ThreadLocal<>();
@Nullable
private final SyncInstance mySync;
private final StructureManager myStructureManager;
private final RowManager myRowManager;
private final boolean myAuto;
private Logger myLog;
@NotNull
private String myPrefix = "";
private final Deque myPrefixStack = new ArrayDeque<>();
public SyncLogger(Logger log, @Nullable SyncInstance sync, StructureManager structureManager, RowManager rowManager, boolean auto) {
myLog = log;
mySync = sync;
myStructureManager = structureManager;
myRowManager = rowManager;
myAuto = auto;
}
/**
* Retrieves SyncLogger from the thread-local storage. Must only be called during synchronization; if called
* at any other time, throws an {@link UnsupportedOperationException}.
*
*
The returned SyncLogger delegates to {@link Logger} for the current {@link StructureSynchronizer} class.
* */
@NotNull
public static SyncLogger get() {
SyncLogger slog = SYNC_LOGGER_THREAD_LOCAL.get();
if (slog == null) throw new IllegalStateException("SyncLogger not installed - maybe not in synchronization thread?");
return slog;
}
/**
* This is internal method that is used by the synchronization subsystem. Do not use this method. Call from threads
* other than the synchronization thread will result in {@link UnsupportedOperationException} being thrown.
* */
@Internal
public static void set(@Nullable SyncLogger syncLog) {
ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
if (!SyncLogger.class.getClassLoader().equals(threadClassLoader)) {
throw new UnsupportedOperationException();
}
SYNC_LOGGER_THREAD_LOCAL.set(syncLog);
}
public static boolean isInfo() {
return get().getLogger().isInfoEnabled();
}
public static boolean isDebug() {
return get().getLogger().isDebugEnabled();
}
@NotNull
public String getPrefix() {
return myPrefix;
}
public void setPrefix(@NotNull String prefix) {
myPrefix = prefix == null ? "" : prefix;
}
public void pushPrefix(@NotNull String prefixToAppend) {
myPrefixStack.push(myPrefix.length());
setPrefix(myPrefix + " " + prefixToAppend);
}
public void popPrefix() {
myPrefix = myPrefix.substring(0, myPrefixStack.pop());
}
public Logger getLogger() {
return myLog;
}
public void setLogger(Logger logger) {
myLog = logger;
}
public boolean isOneTimeSync() {
return mySync != null && mySync.getInstanceId() == 0L;
}
public boolean isAutoSync() {
return myAuto;
}
public void info(Object... msgs) {
if (myLog.isInfoEnabled()) myLog.info(createLogMessage(msgs));
}
public void infoException(Throwable e, Object... msgs) {
if (myLog.isInfoEnabled()) myLog.info(createLogMessage(msgs), e);
}
public void debug(Object... msgs) {
if (myLog.isDebugEnabled()) myLog.debug(createLogMessage(msgs));
}
public void debugException(@Nullable Throwable ex, Object... msgs) {
if (myLog.isDebugEnabled()) {
if (ex != null) myLog.debug(createLogMessage(msgs), ex);
else myLog.debug(createLogMessage(msgs));
}
}
public void warn(Object... msgs) {
if (myLog.isWarnEnabled()) myLog.warn(createLogMessage(msgs));
}
public void warnException(@Nullable Throwable ex, Object... msgs) {
if (myLog.isWarnEnabled()) {
if (ex != null) myLog.warn(createLogMessage(msgs), ex);
else myLog.warn(createLogMessage(msgs));
}
}
public void warnExceptionIfDebug(@Nullable Throwable ex, Object... msgs) {
if (myLog.isWarnEnabled()) {
if (myLog.isDebugEnabled()) {
warnException(ex, msgs);
} else {
if (ex != null) warn(msgs, ex.getMessage());
else warn(msgs);
}
}
}
public void error(Object... msgs) {
if (myLog.isErrorEnabled()) myLog.error(createLogMessage(msgs));
}
public void errorException(@Nullable Throwable e, Object... msgs) {
if (myLog.isErrorEnabled()) {
if (e != null) myLog.error(createLogMessage(msgs), e);
else myLog.error(createLogMessage(msgs));
}
}
@NotNull
public String createLogMessage(Object... msgs) {
StringBuilder msgBuilder = new StringBuilder(myPrefix);
appendLogMessages(msgBuilder, msgs);
return msgBuilder.toString();
}
private static void appendLogMessages(StringBuilder sb, Object... msgs) {
if (msgs != null) {
for (Object msg : msgs) {
if (msg instanceof Object[]) appendLogMessages(sb, (Object[])msg);
else {
if (sb.length() > 0 && !isNoSpaceBefore(msg)) sb.append(' ');
sb.append(msg);
}
}
}
}
private static boolean isNoSpaceBefore(Object msg) {
char c = msg instanceof Character ? ((Character) msg).charValue() : msg instanceof String && !((String) msg).isEmpty() ? ((String) msg).charAt(0) : 0;
int type = Character.getType(c);
return c != 0 && (type == Character.OTHER_PUNCTUATION || type == Character.CONTROL);
}
public final String defaultPrefix() {
if (mySync == null) return "";
StringBuilder res = new StringBuilder();
StructureSynchronizer synchronizer = mySync.getSynchronizer();
String syncLabel;
if (synchronizer != null) {
syncLabel = synchronizer.getDescriptor().getLabel();
if (syncLabel == null || syncLabel.isEmpty()) syncLabel = synchronizer.getClass().getName();
} else {
syncLabel = " " + mySync.getSynchronizerModuleKey();
}
long syncId = mySync.getInstanceId();
boolean oneTime = isOneTimeSync();
res.append(syncLabel)
.append(
myAuto ? " autosync" :
oneTime ? " one-time sync"
: " full sync");
if (!oneTime) res.append(" #").append(syncId);
res.append(" for structure ");
return StructureUtil.appendDebugStructureString(mySync.getStructureId(), myStructureManager, res).toString();
}
@NotNull
public String username() { //j6 ok
String userKey = mySync != null ? mySync.getUserKey() : StructureAuth.getUserKey();
String userName = StructureUtil.getUserNameByKey(userKey);
return userName == null ? "(null)" : userName; //j6 ok
}
@NotNull
public String issue(Long issueId) {
if (myLog.isDebugEnabled()) {
// In debug, forests and other info concerning issue IDs may be printed out, so having issue ID is beneficial
return StructureUtil.getDebugIssueString(issueId);
}
// Otherwise, ID won't help much, having only keys will clutter logs less
if (issueId == null) return "null";
String s = StructureUtil.getDebugIssueKey(issueId);
return s != null ? s : String.valueOf(issueId);
}
public StringBuilder appendIssue(Long issueId, StringBuilder sb) {
if (myLog.isDebugEnabled()) {
// In debug, forests and other info concerning issue IDs may be printed out, so having issue ID is beneficial
return StructureUtil.appendDebugIssueString(issueId, StructureUtil.getDebugIssueKey(issueId), sb);
}
// Otherwise, ID won't help much, having only keys will clutter logs less
if (issueId == null) return sb.append("null");
String s = StructureUtil.getDebugIssueKey(issueId);
return sb.append(s != null ? s : String.valueOf(issueId));
}
@NotNull
public String issue(@Nullable Issue issue) {
if (myLog.isDebugEnabled()) {
// In debug, forests and other info concerning issue IDs may be printed out, so having issue ID is beneficial
return StructureUtil.getDebugIssueString(issue);
}
// Otherwise, ID won't help much, having only keys will clutter logs less
if (issue == null) return "null";
String s = issue.getKey();
return s != null ? s : "#" + issue.getId();
}
@NotNull
public String issues(@Nullable Iterable extends Issue> issues) {
if (issues == null) {
return "null";
}
StringBuilder sb = new StringBuilder("[");
Iterator sep = concat(singletonIterator(""), cycle(", "));
for (Issue issue : issues) {
sb.append(sep.next()).append(issue(issue));
}
return sb.append("]").toString();
}
public String issues(@Nullable LongIterable issues) {
if (issues == null) return "null";
StringBuilder sb = new StringBuilder("[");
Iterator sep = concat(singletonIterator(""), cycle(", "));
for (LongIterator issueIt : issues) {
appendIssue(issueIt.value(), sb.append(sep.next()));
}
return sb.append("]").toString();
}
@NotNull
public String structure(final long structureId) {
return StructureUtil.getDebugStructureString(structureId, myStructureManager);
}
public Object selectBySyncMode(Object ifAuto, Object ifResync, Object ifOneTime) {
return myAuto ? ifAuto : isOneTimeSync() ? ifOneTime : ifResync;
}
public StringBuilder appendForest(Forest forest, StringBuilder sb) {
for (int i = 0, size = forest.size(); i < size; ++i) {
long rowId = forest.getRow(i);
if (i > 0) sb.append(',');
sb.append(rowId).append(':').append(forest.getDepth(i)).append(':');
appendItem(rowId, sb);
}
return sb;
}
public String forest(Forest forest) {
return appendForest(forest, new StringBuilder()).toString();
}
public StringBuilder appendItemForest(ItemForest itemForest, StringBuilder sb) {
Forest forest = itemForest.getForest();
for (int i = 0, size = forest.size(); i < size; ++i) {
long rowId = forest.getRow(i);
if (i > 0) sb.append(',');
sb.append(rowId).append(':').append(forest.getDepth(i)).append(':');
try {
appendItem(itemForest.getRow(rowId).getItemId(), sb);
} catch (MissingRowException e) {
sb.append("');
}
}
return sb;
}
public String itemForest(ItemForest itemForest) {
return appendItemForest(itemForest, new StringBuilder()).toString();
}
public StringBuilder appendRows(LongIterable rowIds, StringBuilder sb) {
boolean appendComma = false;
for (LongIterator rowIdIt : rowIds) {
long rowId = rowIdIt.value();
if (appendComma) {
sb.append(',');
} else {
appendComma = true;
}
sb.append(rowId).append(':');
appendItem(rowId, sb);
}
return sb;
}
public StringBuilder appendItem(long rowId, StringBuilder sb) {
try {
return appendItem(myRowManager.getRow(rowId).getItemId(), sb);
} catch (MissingRowException e) {
return sb.append("');
}
}
public StringBuilder appendItem(ItemIdentity itemId, StringBuilder sb) {
if (CoreIdentities.isIssue(itemId)) {
return appendIssue(itemId.getLongId(), sb);
}
sb.append(itemId.getItemType()).append("/");
if (itemId.isLongId()) sb.append(itemId.getLongId());
else sb.append(itemId.getStringId());
return sb;
}
public String row(long rowId) {
if (rowId == 0) return "0";
else if (rowId < 0) return "r" + rowId;
try {
ItemIdentity itemId = myRowManager.getRow(rowId).getItemId();
StringBuilder sb = new StringBuilder();
appendItem(itemId, sb);
sb.append(" (row ").append(rowId).append(')');
return sb.toString();
} catch (MissingRowException e) {
return "';
}
}
public String rows(LongIterable rowIds) {
return appendRows(rowIds, new StringBuilder()).toString();
}
/**
* Contains standard error descriptions for common StructureExceptions
* @return a specific string if the exception represents a common known case, null otherwise (generic message should be used)
* */
@Nullable
public String warnStructureException(StructureException e) {
switch (e.getError()) {
case STRUCTURE_NOT_EXISTS_OR_NOT_ACCESSIBLE:
return warnAndReturn("cannot run because the structure does not exist or is not accessible for user", username());
case FOREST_CHANGE_PROHIBITED_BY_PARENT_PERMISSIONS:
return warnAndReturn("Error while synchronizing issue", row(e.getIssue()), ":", e.getProblemDetails());
case STRUCTURE_EDIT_DENIED:
return warnAndReturn("cannot run under user", username(), "because he or she does not have permissions to edit the structure");
case STRUCTURE_PLUGIN_ACCESS_DENIED:
return warnAndReturn("cannot run under", username(), ':', "Structure plugin is not enabled for this user");
default:
warnExceptionIfDebug(e, "encountered a problem");
return null;
}
}
private String warnAndReturn(Object... msgs) {
String message = createLogMessage(msgs);
warn(message);
return message;
}
}