summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/logging/CallerFinder.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/logging/CallerFinder.java')
-rw-r--r--java/com/google/gerrit/server/logging/CallerFinder.java238
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;
+ }
+}