diff options
Diffstat (limited to 'java/com/google/gerrit/server/logging/CallerFinder.java')
-rw-r--r-- | java/com/google/gerrit/server/logging/CallerFinder.java | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/logging/CallerFinder.java b/java/com/google/gerrit/server/logging/CallerFinder.java new file mode 100644 index 0000000000..c27dbbb05c --- /dev/null +++ b/java/com/google/gerrit/server/logging/CallerFinder.java @@ -0,0 +1,238 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// 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.google.gerrit.server.logging; + +import static com.google.common.flogger.LazyArgs.lazy; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.LazyArg; +import java.util.Optional; + +/** + * Utility to compute the caller of a method. + * + * <p>In the logs we see for each entry from where it was triggered (class/method/line) but in case + * the logging is done in a utility method or inside of a module this doesn't tell us from where the + * action was actually triggered. To get this information we could included the stacktrace into the + * logs (by calling {@link + * com.google.common.flogger.LoggingApi#withStackTrace(com.google.common.flogger.StackSize)} but + * sometimes there are too many uninteresting stacks so that this would blow up the logs too much. + * In this case CallerFinder can be used to find the first interesting caller from the current + * stacktrace by specifying the class that interesting callers invoke as target. + * + * <p>Example: + * + * <p>Index queries are executed by the {@code query(List<String>, List<Predicate<T>>)} method in + * {@link com.google.gerrit.index.query.QueryProcessor}. At this place the index query is logged but + * from the log we want to see which code triggered this index query. + * + * <p>E.g. the stacktrace could look like this: + * + * <pre> + * GroupQueryProcessor(QueryProcessor<T>).query(List<String>, List<Predicate<T>>) line: 216 + * GroupQueryProcessor(QueryProcessor<T>).query(List<Predicate<T>>) line: 188 + * GroupQueryProcessor(QueryProcessor<T>).query(Predicate<T>) line: 171 + * InternalGroupQuery(InternalQuery<T>).query(Predicate<T>) line: 81 + * InternalGroupQuery.getOnlyGroup(Predicate<InternalGroup>, String) line: 67 + * InternalGroupQuery.byName(NameKey) line: 50 + * GroupCacheImpl$ByNameLoader.load(String) line: 166 + * GroupCacheImpl$ByNameLoader.load(Object) line: 1 + * LocalCache$LoadingValueReference<K,V>.loadFuture(K, CacheLoader<? super K,V>) line: 3527 + * ... + * </pre> + * + * <p>The first interesting caller is {@code GroupCacheImpl$ByNameLoader.load(String) line: 166}. To + * find this caller from the stacktrace we could specify {@link + * com.google.gerrit.server.query.group.InternalGroupQuery} as a target since we know that all + * internal group queries go through this class: + * + * <pre> + * CallerFinder.builder() + * .addTarget(InternalGroupQuery.class) + * .build(); + * </pre> + * + * <p>Since in some places {@link com.google.gerrit.server.query.group.GroupQueryProcessor} may also + * be used directly we can add it as a secondary target to catch these callers as well: + * + * <pre> + * CallerFinder.builder() + * .addTarget(InternalGroupQuery.class) + * .addTarget(GroupQueryProcessor.class) + * .build(); + * </pre> + * + * <p>However since {@link com.google.gerrit.index.query.QueryProcessor} is also responsible to + * execute other index queries (for changes, accounts, projects) we would need to add the classes + * for them as targets too. Since there are common base classes we can simply specify the base + * classes and request matching of subclasses: + * + * <pre> + * CallerFinder.builder() + * .addTarget(InternalQuery.class) + * .addTarget(QueryProcessor.class) + * .matchSubClasses(true) + * .build(); + * </pre> + * + * <p>Another special case is if the entry point is always an inner class of a known interface. E.g. + * {@link com.google.gerrit.server.permissions.PermissionBackend} is the entry point for all + * permission checks but they are done through inner classes, e.g. {@link + * com.google.gerrit.server.permissions.PermissionBackend.ForProject}. In this case matching of + * inner classes must be enabled as well: + * + * <pre> + * CallerFinder.builder() + * .addTarget(PermissionBackend.class) + * .matchSubClasses(true) + * .matchInnerClasses(true) + * .build(); + * </pre> + * + * <p>Finding the interesting caller requires specifying the entry point class as target. This may + * easily break when code is refactored and hence should be used only with care. It's recommended to + * use this only when the corresponding code is relatively stable and logging the caller information + * brings some significant benefit. + * + * <p>Based on {@link com.google.common.flogger.util.CallerFinder}. + */ +@AutoValue +public abstract class CallerFinder { + public static Builder builder() { + return new AutoValue_CallerFinder.Builder() + .matchSubClasses(false) + .matchInnerClasses(false) + .skip(0); + } + + /** + * The target classes for which the caller should be found, in the order in which they should be + * checked. + * + * @return the target classes for which the caller should be found + */ + public abstract ImmutableList<Class<?>> targets(); + + /** + * Whether inner classes should be matched. + * + * @return whether inner classes should be matched + */ + public abstract boolean matchSubClasses(); + + /** + * Whether sub classes of the target classes should be matched. + * + * @return whether sub classes of the target classes should be matched + */ + public abstract boolean matchInnerClasses(); + + /** + * The minimum number of calls known to have occurred between the first call to the target class + * and the call of {@link #findCaller()}. If in doubt, specify zero here to avoid accidentally + * skipping past the caller. + * + * @return the number of stack elements to skip when computing the caller + */ + public abstract int skip(); + + @AutoValue.Builder + public abstract static class Builder { + abstract ImmutableList.Builder<Class<?>> targetsBuilder(); + + public Builder addTarget(Class<?> target) { + targetsBuilder().add(target); + return this; + } + + public abstract Builder matchSubClasses(boolean matchSubClasses); + + public abstract Builder matchInnerClasses(boolean matchInnerClasses); + + public abstract Builder skip(int skip); + + public abstract CallerFinder build(); + } + + public LazyArg<String> findCaller() { + return lazy( + () -> + targets().stream() + .map(t -> findCallerOf(t, skip() + 1)) + .filter(Optional::isPresent) + .findFirst() + .map(Optional::get) + .orElse("unknown")); + } + + private Optional<String> findCallerOf(Class<?> target, int skip) { + // Skip one additional stack frame because we create the Throwable inside this method, not at + // the point that this method was invoked. + skip++; + + StackTraceElement[] stack = new Throwable().getStackTrace(); + + // Note: To avoid having to reflect the getStackTraceDepth() method as well, we assume that we + // will find the caller on the stack and simply catch an exception if we fail (which should + // hardly ever happen). + boolean foundCaller = false; + try { + for (int index = skip; ; index++) { + StackTraceElement element = stack[index]; + if (isCaller(target, element.getClassName(), matchSubClasses())) { + foundCaller = true; + } else if (foundCaller) { + return Optional.of(element.toString()); + } + } + } catch (Exception e) { + // This should only happen if a) the caller was not found on the stack + // (IndexOutOfBoundsException) b) a class that is mentioned in the stack was not found + // (ClassNotFoundException), however we don't want anything to be thrown from here. + return Optional.empty(); + } + } + + private boolean isCaller(Class<?> target, String className, boolean matchSubClasses) + throws ClassNotFoundException { + if (matchSubClasses) { + Class<?> clazz = Class.forName(className); + while (clazz != null) { + if (Object.class.getName().equals(clazz.getName())) { + break; + } + + if (isCaller(target, clazz.getName(), false)) { + return true; + } + clazz = clazz.getSuperclass(); + } + } + + if (matchInnerClasses()) { + int i = className.indexOf('$'); + if (i > 0) { + className = className.substring(0, i); + } + } + + if (target.getName().equals(className)) { + return true; + } + + return false; + } +} |