Merge "Update CodeMirror to 5.37.0"
diff --git a/.bazelproject b/.bazelproject
index e3a7a9c..8a726eb 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -4,6 +4,7 @@
 
 directories:
   .
+  -bin
   -eclipse-out
   -contrib
   -gerrit-package-plugins
diff --git a/0001-Replace-native-http-git-_archive-with-Skylark-rules.patch b/0001-Replace-native-http-git-_archive-with-Skylark-rules.patch
new file mode 100644
index 0000000..3ccf5cd
--- /dev/null
+++ b/0001-Replace-native-http-git-_archive-with-Skylark-rules.patch
@@ -0,0 +1,133 @@
+Date: Wed, 30 May 2018 21:22:18 +0200
+Subject: [PATCH] Replace native {http,git}_archive with Skylark rules
+
+See [1] for more details.
+
+Test Plan:
+
+* Apply this CL on Bazel master: [2] and build bazel
+* Run with this custom built bazel version:
+
+  $ bazel test //javatests/...
+  $ bazel test //closure/...
+
+[1] https://groups.google.com/d/topic/bazel-discuss/dO2MHQLwJF0/discussion
+[2] https://bazel-review.googlesource.com/#/c/bazel/+/55932/
+---
+ closure/repositories.bzl | 23 ++++++++++++-----------
+ 1 file changed, 12 insertions(+), 11 deletions(-)
+
+diff --git a/closure/repositories.bzl b/closure/repositories.bzl
+index 9b84a72..2816fb6 100644
+--- closure/repositories.bzl
++++ closure/repositories.bzl
+@@ -14,6 +14,7 @@
+ 
+ """External dependencies for Closure Rules."""
+ 
++load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
+ load("//closure/private:java_import_external.bzl", "java_import_external")
+ load("//closure/private:platform_http_file.bzl", "platform_http_file")
+ load("//closure:filegroup_external.bzl", "filegroup_external")
+@@ -405,7 +406,7 @@ def com_google_common_html_types():
+   )
+ 
+ def com_google_common_html_types_html_proto():
+-  native.http_file(
++  http_file(
+       name = "com_google_common_html_types_html_proto",
+       sha256 = "6ece202f11574e37d0c31d9cf2e9e11a0dbc9218766d50d211059ebd495b49c3",
+       urls = [
+@@ -633,7 +634,7 @@ def com_google_javascript_closure_compiler():
+ 
+ def com_google_javascript_closure_library():
+   # After updating: bazel run //closure/library:regenerate -- "$PWD"
+-  native.new_http_archive(
++  http_archive(
+       name = "com_google_javascript_closure_library",
+       urls = [
+           "https://mirror.bazel.build/github.com/google/closure-library/archive/v20180405.tar.gz",
+@@ -658,7 +659,7 @@ def com_google_jsinterop_annotations():
+ 
+ def com_google_protobuf():
+   # Note: Protobuf 3.6.0+ is going to use C++11
+-  native.http_archive(
++  http_archive(
+       name = "com_google_protobuf",
+       strip_prefix = "protobuf-3.5.1",
+       sha256 = "826425182ee43990731217b917c5c3ea7190cfda141af4869e6d4ad9085a740f",
+@@ -669,7 +670,7 @@ def com_google_protobuf():
+   )
+ 
+ def com_google_protobuf_js():
+-  native.new_http_archive(
++  http_archive(
+       name = "com_google_protobuf_js",
+       urls = [
+           "https://mirror.bazel.build/github.com/google/protobuf/archive/v3.5.1.tar.gz",
+@@ -722,7 +723,7 @@ def com_google_template_soy():
+   )
+ 
+ def com_google_template_soy_jssrc():
+-  native.new_http_archive(
++  http_archive(
+       name = "com_google_template_soy_jssrc",
+       sha256 = "c76ab4cb6e46a7c76336640b3c40d6897b420209a6c0905cdcd32533dda8126a",
+       urls = [
+@@ -757,7 +758,7 @@ def com_squareup_javapoet():
+   )
+ 
+ def fonts_noto_hinted_deb():
+-  native.http_file(
++  http_file(
+       name = "fonts_noto_hinted_deb",
+       urls = [
+           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/fonts-noto/fonts-noto-hinted_20161116-1_all.deb",
+@@ -767,7 +768,7 @@ def fonts_noto_hinted_deb():
+   )
+ 
+ def fonts_noto_mono_deb():
+-  native.http_file(
++  http_file(
+       name = "fonts_noto_mono_deb",
+       urls = [
+           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/fonts-noto/fonts-noto-mono_20161116-1_all.deb",
+@@ -801,7 +802,7 @@ def javax_inject():
+   )
+ 
+ def libexpat_amd64_deb():
+-  native.http_file(
++  http_file(
+       name = "libexpat_amd64_deb",
+       urls = [
+           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/e/expat/libexpat1_2.1.0-6+deb8u3_amd64.deb",
+@@ -811,7 +812,7 @@ def libexpat_amd64_deb():
+   )
+ 
+ def libfontconfig_amd64_deb():
+-  native.http_file(
++  http_file(
+       name = "libfontconfig_amd64_deb",
+       urls = [
+           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/fontconfig/libfontconfig1_2.11.0-6.3+deb8u1_amd64.deb",
+@@ -821,7 +822,7 @@ def libfontconfig_amd64_deb():
+   )
+ 
+ def libfreetype_amd64_deb():
+-  native.http_file(
++  http_file(
+       name = "libfreetype_amd64_deb",
+       urls = [
+           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/f/freetype/libfreetype6_2.5.2-3+deb8u1_amd64.deb",
+@@ -831,7 +832,7 @@ def libfreetype_amd64_deb():
+   )
+ 
+ def libpng_amd64_deb():
+-  native.http_file(
++  http_file(
+       name = "libpng_amd64_deb",
+       urls = [
+           "https://mirror.bazel.build/http.us.debian.org/debian/pool/main/libp/libpng/libpng12-0_1.2.50-2+deb8u2_amd64.deb",
+-- 
+2.16.3
+
diff --git a/BUILD b/BUILD
index 2258a37..91e2dec 100644
--- a/BUILD
+++ b/BUILD
@@ -20,7 +20,10 @@
     visibility = ["//visibility:public"],
 )
 
-pkg_war(name = "gerrit")
+pkg_war(
+    name = "gerrit",
+    ui = "polygerrit",
+)
 
 pkg_war(
     name = "headless",
diff --git a/Documentation/BUILD b/Documentation/BUILD
index 2e6f4bc..4177f51 100644
--- a/Documentation/BUILD
+++ b/Documentation/BUILD
@@ -71,13 +71,13 @@
     name = "index",
     srcs = SRCS,
     outs = ["index.jar"],
-    cmd = "$(location //lib/asciidoctor:doc_indexer) " +
+    cmd = "$(location //java/com/google/gerrit/asciidoctor:doc_indexer) " +
           "-o $(OUTS) " +
           "--prefix \"%s/\" " % DOC_DIR +
           "--in-ext \".txt\" " +
           "--out-ext \".html\" " +
           "$(SRCS)",
-    tools = ["//lib/asciidoctor:doc_indexer"],
+    tools = ["//java/com/google/gerrit/asciidoctor:doc_indexer"],
 )
 
 # For the same srcs, we can have multiple genasciidoc_zip rules, but only one
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7ed0e17..3f97120 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -753,6 +753,7 @@
 * `"diff"`: default is `10m` (10 MiB of memory)
 * `"diff_intraline"`: default is `10m` (10 MiB of memory)
 * `"diff_summary"`: default is `10m` (10 MiB of memory)
+* `"external_ids_map"`: default is `2` and should not be changed
 * `"groups"`: default is unlimited
 * `"groups_byname"`: default is unlimited
 * `"groups_byuuid"`: default is unlimited
@@ -762,6 +763,16 @@
 If set to 0 the cache is disabled. Entries are removed immediately
 after being stored by the cache. This is primarily useful for testing.
 
+[[cache.name.expireFromMemoryAfterAccess]]cache.<name>.expireFromMemoryAfterAccess::
++
+Time after last access to automatically expire entries from an in-memory
+cache. If 0 or not specified, entries are never expired in this manner.
+Values may use unit suffixes as in link:#cache.name.maxAge[maxAge].
++
+This option only applies to in-memory caches; persistent cache values are
+not expired in this manner, and are only pruned via
+link:#cache.name.diskLimit[diskLimit].
+
 [[cache.name.diskLimit]]cache.<name>.diskLimit::
 +
 Total size in bytes of the keys and values stored on disk. Caches that
@@ -844,6 +855,16 @@
 This should significantly speed up change reindexing, especially
 full offline reindexing.
 
+cache `"external_ids_map"`::
++
+A singleton cache whose sole entry is a map of the parsed representation
+of link:config-accounts.html#external-ids[all current external IDs]. The
+cache may temporarily contain 2 entries, but the second one is promptly
+expired.
++
+It is not recommended to change the attributes of this cache away from
+the defaults.
+
 cache `"git_tags"`::
 +
 If branch or reference level READ access controls are used, this
@@ -2973,10 +2994,13 @@
 === Section elasticsearch
 
 WARNING: The Elasticsearch support has only been tested with Elasticsearch
-version 2.4.x. Support for other versions is not guaranteed.
+versions 2.4, 5.6 and 6.2. Support for other versions is not guaranteed.
 
-Open and closed changes are indexed in a single index, separated
-into types `open_changes` and `closed_changes` respectively.
+Open and closed changes are indexed in a single index, separated into types
+`open_changes` and `closed_changes` respectively, if using Elasticsearch
+versions 2.4 or 5.6. Open and closed changes are merged into the default `_doc`
+type otherwise. The latter is also used for accounts and groups indices starting
+with Elasticsearch 6.2.
 
 [[elasticsearch.prefix]]elasticsearch.prefix::
 +
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 91e20cd..cf78c6d 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -275,8 +275,8 @@
 sticky approvals, reducing turn-around for trivial cleanups prior to
 submitting a change. Defaults to false.
 
-[[label_copyAllScoresOnMergeCommitFirstParentUpdate]]
-=== `label.Label-Name.copyAllScoresOnMergeCommitFirstParentUpdate`
+[[label_copyAllScoresOnMergeFirstParentUpdate]]
+=== `label.Label-Name.copyAllScoresOnMergeFirstParentUpdate`
 
 This policy is useful if you don't want to trigger CI or human
 verification again if your target branch moved on but the feature
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index ccad352..055ebcb 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -244,6 +244,12 @@
   bazel test --test_tag_filters=-flaky //...
 ----
 
+To exclude tests that require a Docker host:
+
+----
+  bazel test --test_tag_filters=-docker //...
+----
+
 To ignore cached test results:
 
 ----
@@ -268,6 +274,15 @@
 * server
 * ssh
 
+[[elasticsearch]]
+=== Elasticsearch
+
+Successfully running the elasticsearch tests may require setting the local
+link:https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html[virtual memory].
+
+Bazel link:https://github.com/bazelbuild/bazel/issues/3476[does not currently make container failures visible],
+if any.
+
 == Dependencies
 
 Dependency JARs are normally downloaded as needed, but you can
@@ -363,54 +378,19 @@
 `lib/jgit/jgit.bzl` setting LOCAL_JGIT_REPO to a directory holding a
 JGit repository.
 
-[[local-action-cache]]
+[[bazel-local-caches]]
 
-To accelerate builds, local action cache can be activated. Note, that this
-experimental feature is not activated per default and only available since
-Bazel version 0.7.
+To accelerate builds, several caches are activated per default:
 
-To activate the local action cache, create accessible cache directory:
+* ~/.gerritcodereview/bazel-cache/downloaded-artifacts
+* ~/.gerritcodereview/bazel-cache/repository
+* ~/.gerritcodereview/bazel-cache/cas
 
-----
- mkdir -p ~/.gerritcodereview/bazel-cache/cas
-----
+Currently none of these caches have a maximum size limit. See
+link:https://github.com/bazelbuild/bazel/issues/5139[this bazel issue] for
+details. Users should watch the cache sizes and clean them manually if
+necessary.
 
-and add these lines to your `~/.bazelrc` file:
-
-----
-build --experimental_local_disk_cache_path=/home/<user>/.gerritcodereview/bazel-cache/cas
-build --experimental_local_disk_cache
-build --experimental_strict_action_env
-build --action_env=PATH
-----
-
-[NOTE] `experimental_local_disk_cache_path` must be absolute path. Expansion of `~` is
-unfortunately not supported yet. This is also the reason why we can't activate this
-feature by default yet (by adjusting tools/bazel.rc file).
-
-[[repository_cache]]
-
-To accelerate fetches, local repository cache can be activated. This cache is
-only used for rules_closure external repository and transitive dependendcies.
-That's because rules_closure uses standard Bazel download facility. For all
-other gerrit dependencies, the download_artifacts repository cache is used
-already.
-
-To activate the local repository cache, create accessible cache directory:
-
-----
- mkdir -p ~/.gerritcodereview/bazel-cache/repository
-----
-
-and add this line to your `~/.bazelrc` file:
-
-----
-build --experimental_repository_cache=/home/<user>/.gerritcodereview/bazel-cache/repository
-----
-
-[NOTE] `experimental_repository_cache` must be absolute path. Expansion of `~` is
-unfortunately not supported yet. This is also the reason why we can't activate this
-feature by default yet (by adjusting tools/bazel.rc file).
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index c6cadbb..8acd663 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -165,7 +165,8 @@
 To format Java source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
 tool (version 1.5), and to format Bazel BUILD and WORKSPACE files the
-link:https://github.com/bazelbuild/buildifier[`buildifier`] tool (version 0.6.0).
+link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
+tool (version 0.11.1).
 These tools automatically apply format according to the style guides; this
 streamlines code review by reducing the need for time-consuming, tedious,
 and contentious discussions about trivial issues like whitespace.
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index 13216ec..8bedd08 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -1,7 +1,10 @@
 = Gerrit Code Review - IntelliJ Setup
 
 == Prerequisites
-You need an installation of IntelliJ of version 2016.2.
+You need an installation of IntelliJ version 2016.2 or later. The latest version
+might not yet be in-sync with the Bazel plugin for IntelliJ. It usually becomes
+so quite quickly after new IDEA versions get released, though. It should then be
+possible to use the fairly latest IntelliJ release with an updated Bazel plugin.
 
 In addition, Java 8 must be specified on your path or via `JAVA_HOME` so that
 building with Bazel via the Bazel plugin is possible.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index e6a6227..a59c08d 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -765,7 +765,7 @@
 [source, java]
 ----
 public class SshModule extends AbstractModule {
-  private static final Logger log = LoggerFactory.getLogger(SshModule.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   protected void configure() {
@@ -778,7 +778,7 @@
   public static class BanOptions implements DynamicOptions.DynamicBean {
     @Option(name = "--log", aliases = { "-l" }, usage = "Say Hello in the Log")
     private void parse(String arg) {
-      log.error("Say Hello in the Log " + arg);
+      logger.atSevere().log("Say Hello in the Log %s", arg);
     }
   }
 ----
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 5c24731..a170e07 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -1,9 +1,7 @@
-= Gerrit Code Review - Developer Setup
+= Gerrit Code Review: Developer Setup
 
-Google Bazel is needed to compile the code, and an SQL database to
-house the review metadata.  H2 is recommended for development
-databases, as it requires no external server process.
-
+To build a developer instance, you'll need link:https://bazel.build/[Bazel] to
+compile the code.
 
 == Getting the Source
 
@@ -21,59 +19,39 @@
 [[compile_project]]
 == Compiling
 
-Please refer to <<dev-bazel#,Building with Bazel>>.
-
-== Switching between branches
-
-When switching between branches with `git checkout`, be aware that
-submodule revisions are not altered.  This may result in the wrong
-plugin revisions being present, unneeded plugins being present, or
-expected plugins being missing.
-
-After switching branches, make sure the submodules are at the correct
-revisions for the new branch with the commands:
-
-----
-  git submodule update
-  git clean -fdx
-----
-
-CAUTION: If you decide to store your Eclipse/IntelliJ project files in the
-Gerrit source directories, executing `git clean -fdx` will remove them and hence
-screw up your project.
-
+For details, see <<dev-bazel#,Building with Bazel>>.
 
 == Configuring Eclipse
 
-To use the Eclipse IDE for development, please see
+To use the Eclipse IDE for development, see
 link:dev-eclipse.html[Eclipse Setup].
 
-For details on how to configure the Eclipse workspace with Bazel,
-refer to: link:dev-bazel.html#eclipse[Eclipse integration with Bazel].
-
+To configure the Eclipse workspace with Bazel, see
+link:dev-bazel.html#eclipse[Eclipse integration with Bazel].
 
 == Configuring IntelliJ IDEA
 
-Please refer to <<dev-intellij#,IntelliJ Setup>> for detailed
-instructions.
+See <<dev-intellij#,IntelliJ Setup>> for details.
 
-== Mac OS X
+== MacOS
 
-On Mac OS X ensure "Java For Mac OS X 10.5 Update 4" (or later) has
-been installed, and that `JAVA_HOME` is set to the
+On MacOS, ensure that "Java for MacOS X 10.5 Update 4" (or higher) is installed
+and that `JAVA_HOME` is set to the
 link:install.html#Requirements[required Java version].
 
 Java installations can typically be found in
 "/System/Library/Frameworks/JavaVM.framework/Versions".
 
-You can check the installed Java version by running `java -version` in
-the terminal.
+To check the installed version of Java, open a terminal window and run:
+
+`java -version`
 
 [[init]]
 == Site Initialization
 
-After compiling <<compile_project,(above)>>, run Gerrit's 'init' command to
-create a testing site for development use:
+After you compile the project <<compile_project,(above)>>, run the Gerrit
+`init`
+command to create a test site:
 
 ----
   $(bazel info output_base)/external/local_jdk/bin/java \
@@ -81,24 +59,26 @@
 ----
 
 [[special_bazel_java_version]]
-NOTE: You must use the same Java version that Bazel used for the build.
-This Java version is available at
-`$(bazel info output_base)/external/local_jdk/bin/java`.
+NOTE: You must use the same Java version that Bazel used for the build, which
+is available at `$(bazel info output_base)/external/local_jdk/bin/java`.
 
-During initialization, make two changes to the default settings:
+During initialization, change two settings from the defaults:
 
-* Change the listen addresses from '*' to 'localhost' to prevent outside
-  connections from contacting the development instance; and
-* Change the auth type from 'OPENID' to 'DEVELOPMENT_BECOME_ANY_ACCOUNT' to
-  allow yourself to create and act as arbitrary test accounts on your
-  development instance.
+*  To ensure the development instance is not externally accessible, change the
+listen addresses from '*' to 'localhost'.
+*  To allow yourself to create and act as arbitrary test accounts on your
+development instance, change the auth type from 'OPENID' to 'DEVELOPMENT_BECOME_ANY_ACCOUNT'.
 
-Continue through init until it completes. The daemon will automatically start in
-the background and a web browser will launch to the start page. From here you
-can sign in as the account created during init, register additional accounts,
-create projects, and more.
+After initializing the test site, Gerrit starts serving in the background. A
+web browser displays the Start page.
 
-When you want to shut down the daemon, simply run:
+On the Start page, you can:
+
+.  Log in as the account you created during the initialization process.
+.  Register additional accounts.
+.  Create projects.
+
+To shut down the daemon, run:
 
 ----
   ../gerrit_testsite/bin/gerrit.sh stop
@@ -108,9 +88,11 @@
 [[localdev]]
 == Working with the Local Server
 
-If you need to create additional accounts on your development instance, click
-'become' in the upper right corner, select 'Switch User', and then register
-a new account.
+To create more accounts on your development instance:
+
+.  Click 'become' in the upper right corner.
+.  Select 'Switch User'.
+.  Register a new account.
 
 Use the `ssh` protocol to clone from and push to the local server. For
 example, to clone a repository that you've created through the admin
@@ -120,34 +102,31 @@
 git clone ssh://username@localhost:29418/projectname
 ----
 
-Then you'll be able to create changes the same way users do, with
+To create changes as users of Gerrit would, run:
 
 ----
 git push origin HEAD:refs/for/master
 ----
 
-
-
 == Testing
 
-
 [[tests]]
-=== Running the Acceptance Tests
+=== Running the acceptance tests
 
-Gerrit has a set of integration tests that test the Gerrit daemon via
-REST, SSH and the git protocol.
+Gerrit contains acceptance tests that validate the Gerrit daemon via REST, SSH,
+and the Git protocol.
 
 A new review site is created for each test and the Gerrit daemon is
-started on that site. When the test has finished the Gerrit daemon is
-shutdown.
+then started on that site. When the test is completed, the Gerrit daemon is
+shut down.
 
-For instructions on running the integration tests with Bazel,
-please refer to:  <<dev-bazel#tests,Running Unit Tests with Bazel>>.
+For instructions on running the acceptance tests with Bazel,
+see <<dev-bazel#tests,Running Unit Tests with Bazel>>.
 
 [[run_daemon]]
 === Running the Daemon
 
-The daemon can be directly launched from the build area, without
+The daemon can be launched directly from the build area, without
 copying to the test site:
 
 ----
@@ -156,133 +135,101 @@
      --console-log
 ----
 
-NOTE: Please refer to <<special_bazel_java_version,this explanation>>
-for details why using `java -jar` isn't sufficient.
+NOTE: To learn why using `java -jar` isn't sufficient, see
+<<special_bazel_java_version,this explanation>>.
 
-If you want to debug the Gerrit server of this test site, you can open a debug
-port (for example port 5005) by inserting
+To debug the Gerrit server of this test site:
+
+.  Open a debug port (such as port 5005). To do so, insert the following code
+immediately after `-jar` in the previous command. To learn how to attach
+IntelliJ, see <<dev-intellij#remote-debug,Debugging a remote Gerrit server>>.
 
 ----
 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
 ----
 
-directly after `-jar` of the previous command. Please refer to
-<<dev-intellij#remote-debug,Debugging a remote Gerrit server>> for instructions
-of how to attach IntelliJ.
-
 === Running the Daemon with Gerrit Inspector
 
 link:dev-inspector.html[Gerrit Inspector] is an interactive scriptable
-environment to inspect and modify internal state of the system.
+environment you can use to inspect and modify the internal state of the system.
 
-This environment is available on the system console after
-the system starts. Leaving the Inspector will shutdown the Gerrit
-instance.
+Gerrit Inspector appears on the system console whenever the system starts.
+Leaving the Inspector shuts down the Gerrit instance.
 
-The environment allows interactive work as well as running of
-Python scripts for troubleshooting.
+To troubleshoot, the Inspector enables interactive work as well as running of
+Python scripts.
 
-Gerrit Inspect can be started by adding '-s' option to the
-command used to launch the daemon:
+To start the Inspector, add the '-s' option to the daemon start command:
 
 ----
   $(bazel info output_base)/external/local_jdk/bin/java \
      -jar bazel-bin/gerrit.war daemon -d ../gerrit_testsite -s
 ----
 
-NOTE: Please refer to <<special_bazel_java_version,this explanation>>
-for details why using `java -jar` isn't sufficient.
+NOTE: To learn why using `java -jar` isn't sufficient, see
+<<special_bazel_java_version,this explanation>>.
 
-Gerrit Inspector examines Java libraries first, then loads
-its initialization scripts and then starts a command line
-prompt on the console:
+Inspector examines Java libraries, loads the initialization scripts, and
+starts a command line prompt on the console:
 
 ----
   Welcome to the Gerrit Inspector
   Enter help() to see the above again, EOF to quit and stop Gerrit
   Jython 2.5.2 (Release_2_5_2:7206, Mar 2 2011, 23:12:06)
-  [OpenJDK 64-Bit Server VM (Sun Microsystems Inc.)] on java1.6.0 running for Gerrit 2.3-rc0-163-g01967ef
+  [OpenJDK 64-Bit Server VM (Sun Microsystems Inc.)] on java1.6.0 running for
+  Gerrit 2.3-rc0-163-g01967ef
   >>>
 ----
 
-With the Inspector enabled Gerrit can be used normally and all
-interfaces (HTTP, SSH etc.) are available.
+When the Inspector is enabled, you can use Gerrit as usual and all
+interfaces (including HTTP and SSH) are available.
 
-Care must be taken not to modify internal state of the system
-when using the Inspector.
+CAUTION: When using the Inspector, be careful not to modify the internal state
+of the system.
 
-=== Querying the Database
+=== Querying the database
 
-The embedded H2 database can be queried and updated from the
-command line.  If the daemon is not currently running:
+The embedded H2 database can be queried and updated from the command line. If
+the daemon is not running, run:
 
 ----
   $(bazel info output_base)/external/local_jdk/bin/java \
      -jar bazel-bin/gerrit.war gsql -d ../gerrit_testsite -s
 ----
 
-NOTE: Please refer to <<special_bazel_java_version,this explanation>>
-for details why using `java -jar` isn't sufficient.
+NOTE: To learn why using `java -jar` isn't sufficient, see
+<<special_bazel_java_version,this explanation>>.
 
-Or, if it is running and the database is in use, connect over SSH
-using an administrator user account:
+Alternatively, if the daemon is running and the database is in use, use an
+administrator user account to connect over SSH:
 
 ----
   ssh -p 29418 user@localhost gerrit gsql
 ----
 
 
-[[debug-javascript]]
-=== Debugging JavaScript
+== Switching between branches
 
-When debugging browser specific issues add `?dbg=1` to the URL so the
-resulting JavaScript more closely matches the Java sources.  The debug
-pages use the GWT pretty format, where function and variable names
-match the Java sources.
+When using `git checkout` without `--recurse-submodules` to switch between
+branches, submodule revisions are not altered, which can result in:
+
+*  Incorrect or unneeded plugin revisions.
+*  Missing plugins.
+
+After you switch branches, ensure that you have the correct versions of
+the submodules.
+
+CAUTION: If you store Eclipse or IntelliJ project files in the Gerrit source
+directories, do *_not_* run `git clean -fdx`. Doing so may remove untracked files and damage your project. For more information, see
+link:https://git-scm.com/docs/git-clean[git-clean].
+
+Run the following:
 
 ----
-  http://localhost:8080/?dbg=1
+  git submodule update
+  git clean -ffd
 ----
 
-
-== Client-Server RPC
-
-The client-server RPC implementation is gwtjsonrpc, not the stock RPC
-system that comes with GWT.  This buys us automatic XSRF protection.
-It also makes all of the messages readable and writable by any JSON
-implementation, facilitating "mashups" and 3rd party clients.
-
-The programming API is virtually identical, except service interfaces
-extend RemoteJsonService instead of RemoteService.
-
-
-== Why GWT?
-
-We like it.  Plus we can write Java code once and run it both in
-the browser and on the server side.
-
-
-== External Links
-
-Google Web Toolkit:
-
-* http://www.gwtproject.org/download.html[Download]
-
-Apache SSHD:
-
-* http://mina.apache.org/sshd/[SSHD]
-
-H2:
-
-* http://www.h2database.com/[H2]
-* http://www.h2database.com/html/grammar.html[SQL Reference]
-
-PostgreSQL:
-
-* http://www.postgresql.org/download/[Download]
-* http://www.postgresql.org/docs/[Documentation]
-
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 0f121a0..cc19b3f 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -1,17 +1,19 @@
 = Gerrit Code Review - Standalone Daemon Installation Guide
 
-[[requirements]]
-== Requirements
-To run the Gerrit service, the following requirements must be met on
-the host:
+[[prerequisites]]
+== Prerequisites
+
+To run the Gerrit service, the following requirement must be met on the host:
 
 * JRE, minimum version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
 
-You'll also need an SQL database to house the review metadata. You have the
-choice of either using the embedded H2 or to host your own MySQL or PostgreSQL.
+By default, Gerrit uses link:note-db.html[NoteDB] as the storage backend. (If
+desired, you can _optionally_ use an external database such as MySQL or
+PostgreSQL.)
 
 [[cryptography]]
 == Configure Java for Strong Cryptography
+
 Support for extra strength cryptographic ciphers: _AES128CTR_, _AES256CTR_,
 _ARCFOUR256_, and _ARCFOUR128_ can be enabled by downloading the _Java
 Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files_
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index a28d062..2df5971 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -770,7 +770,7 @@
 ----
 Gerrit.get('/changes/?q=status:open', function (open) {
   for (var i = 0; i < open.length; i++) {
-    console.log(open.get(i).change_id);
+    console.log(open[i].change_id);
   }
 });
 ----
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index 84deeb5..2464c3a 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -1,64 +1,63 @@
 = Quickstart for Installing Gerrit on Linux
 
-This quickstart shows you how to install Gerrit on a Linux machine.
+This content explains how to install a basic instance of Gerrit on a Linux
+machine.
 
 [NOTE]
 ====
-The installation steps provided in this quickstart are for
-demonstration purposes only. They are not intended for use in a production
-environment.
+This quickstart is provided for demonstration purposes only. The Gerrit instance
+they install must not be used in a production environment.
 
-For a more detailed installation guide, see
+Instead, to install a Gerrit production environment, see
 link:install.html[Standalone Daemon Installation Guide].
 ====
 
-== Before you begin
+== Before you start
 
-To complete this quickstart, you need:
+Be sure you have:
 
-. A Unix-based server such as any of the Linux flavors or BSD.
-. Java SE Runtime Environment version 1.8 or later.
+. A Unix-based server, including any Linux flavor, MacOS, or Berkeley Software
+    Distribution (BSD).
+. Java SE Runtime Environment 1.8 (or higher).
 
 == Download Gerrit
 
 From the Linux machine on which you want to install Gerrit:
 
 . Open a terminal window.
-. Download the Gerrit archive. See
-link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code
-Review - Releases] for a list of available archives.
+. Download the desired Gerrit archive.
 
-The steps in this quickstart used Gerrrit 2.14.2, which you can download using
-a command such as:
+To view previous archives, see
+link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases]. The steps below install Gerrit 2.15.1:
 
 ....
-wget https://www.gerritcodereview.com/download/gerrit-2.14.2.war
+wget https://www.gerritcodereview.com/download/gerrit-2.15.1.war
 ....
 
-NOTE: If you want to build and install Gerrit from the source files, see
-link:dev-readme.html[Developer Setup].
+NOTE: To build and install Gerrit from the source files, see
+link:dev-readme.html[Gerrit Code Review: Developer Setup].
 
 == Install and initialize Gerrit
 
-From the command line, type the following:
+From the command line, enter:
 
 ....
 java -jar gerrit*.war init --batch --dev -d ~/gerrit_testsite
 ....
 
-The preceding command uses two parameters:
+This command takes two parameters:
 
-* `--batch`. This parameter assigns default values to a variety of Gerrit
-  configuration options. To learn more about these configuration options, see
-  link:config-gerrit.html[Configuration].
-* `--dev`. This parameter configures the server to use the authentication
-  option, `DEVELOPMENT_BECOME_ANY_ACCOUNT`. This authentication type makes it
-  easy for you to switch between different users to explore how Gerrit works.
-  To learn more about setting up Gerrit for development, see
-  link:dev-readme.html[Developer Setup].
+* `--batch` assigns default values to several Gerrit configuration
+    options. To learn more about these options, see
+    link:config-gerrit.html[Configuration].
+* `--dev` configures the Gerrit server to use the authentication
+  option, `DEVELOPMENT_BECOME_ANY_ACCOUNT`, which enables you to
+  switch between different users to explore how Gerrit works. To learn more
+  about setting up Gerrit for development, see
+  link:dev-readme.html[Gerrit Code Review: Developer Setup].
 
-This command displays a number of messages in the terminal window. The following
-is an example of these messages:
+While this command executes, status messages are displayed in the terminal
+window. For example:
 
 ....
 Generating SSH host key ... rsa(simple)... done
@@ -67,14 +66,15 @@
 Starting Gerrit Code Review: OK
 ....
 
-The last message you should see is `Starting Gerrit Code Review: OK`. This
-message informs you that the Gerrit service is now running.
+The last message confirms that the Gerrit service is running:
+
+`Starting Gerrit Code Review: OK`.
 
 == Update the listen URL
 
-Another recommended task is to change the URL that Gerrit listens to from `*`
-to `localhost`. This change helps prevent outside connections from contacting
-the instance.
+To prevent outside connections from contacting your new Gerrit instance
+(strongly recommended), change the URL on which Gerrit listens from `*` to
+`localhost`. For example:
 
 ....
 git config --file ~/gerrit_testsite/etc/gerrit.config httpd.listenUrl 'http://localhost:8080'
@@ -83,7 +83,7 @@
 == Restart the Gerrit service
 
 You must restart the Gerrit service for your authentication type and listen URL
-changes to take effect.
+changes to take effect:
 
 ....
 ~/gerrit_testsite/bin/gerrit.sh restart
@@ -91,8 +91,7 @@
 
 == Viewing Gerrit
 
-At this point, you have a basic installation of Gerrit. You can view this
-installation by opening a browser and entering the following URL:
+To view your new basic installation of Gerrit, go to:
 
 ....
 http://localhost:8080
@@ -100,10 +99,10 @@
 
 == Next steps
 
-Through this quickstart, you now have a simple version of Gerrit running on your
-Linux machine. You can use this installation to explore the UI and become
-familiar with some of Gerrit's features. For a more detailed installation guide,
-see link:install.html[Standalone Daemon Installation Guide].
+Now that you have a simple version of Gerrit running, use the installation to
+explore the user interface and learn about Gerrit. For more detailed
+installation instructions, see
+link:[Standalone Daemon Installation Guide](install.html).
 
 GERRIT
 ------
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 640178e..19d3b41 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -68,16 +68,28 @@
 * `query/query_latency`: Successful query latency, accumulated over the life
 of the process.
 
-=== Queue
+=== Core Queues
 
-The metrics below are per queue.
+The following queues support metrics:
 
-* `queue/<queueName>/pool_size`: Current number of threads in the pool
-* `queue/<queueName>/max_pool_size`: Maximum allowed number of threads in the pool
-* `queue/<queueName>/active_threads`: Number of threads that are actively executing tasks
-* `queue/<queueName>/scheduled_tasks`: Number of scheduled tasks in the queue
-* `queue/<queueName>/total_scheduled_tasks_count`: Total number of tasks that have been scheduled
-* `queue/<queueName>/total_completed_tasks_count`: Total number of tasks that have completed execution
+* default `WorkQueue`
+* index batch
+* index interactive
+* receive commits
+* send email
+* ssh batch worker
+* ssh command start
+* ssh interactive worker
+* ssh stream worker
+
+Each queue provides the following metrics:
+
+* `queue/<queue_name>/pool_size`: Current number of threads in the pool
+* `queue/<queue_name>/max_pool_size`: Maximum allowed number of threads in the pool
+* `queue/<queue_name>/active_threads`: Number of threads that are actively executing tasks
+* `queue/<queue_name>/scheduled_tasks`: Number of scheduled tasks in the queue
+* `queue/<queue_name>/total_scheduled_tasks_count`: Total number of tasks that have been scheduled
+* `queue/<queue_name>/total_completed_tasks_count`: Total number of tasks that have completed execution
 
 === SSH sessions
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ac7be2c..8d1b2d8 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5809,7 +5809,8 @@
 The name of the target branch. +
 The `refs/heads/` prefix is omitted.
 |`subject`            ||
-The subject of the change (header line of the commit message).
+The commit message of the change. Comment lines (beginning with `#`) will
+be removed.
 |`topic`              |optional|The topic to which this change belongs.
 |`status`             |optional, default to `NEW`|
 The status of the change (only `NEW` accepted here).
diff --git a/WORKSPACE b/WORKSPACE
index 4dacd78..aaccade 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,5 +1,6 @@
 workspace(name = "gerrit")
 
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
 load("//tools/bzl:maven_jar.bzl", "maven_jar", "GERRIT", "MAVEN_LOCAL")
 load("//lib/codemirror:cm.bzl", "CM_VERSION", "DIFF_MATCH_PATCH_VERSION")
 load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
@@ -11,12 +12,13 @@
     urls = ["https://github.com/bazelbuild/bazel-skylib/archive/2169ae1c374aab4a09aa90e65efe1a3aad4e279b.tar.gz"],
 )
 
-# davido's fork with https://github.com/bazelbuild/rules_closure/pull/235 included
 http_archive(
     name = "io_bazel_rules_closure",
-    sha256 = "314e4eb701696e267cb911609e2e333e321fe641981a33144f460068ff4e1af3",
-    strip_prefix = "rules_closure-0.11.0",
-    url = "https://github.com/davido/rules_closure/archive/0.11.0.tar.gz",
+    build_file_content = "exports_files([\"0001-Replace-native-http-git-_archive-with-Skylark-rules.patch\"])",
+    patches = ["//:0001-Replace-native-http-git-_archive-with-Skylark-rules.patch"],
+    sha256 = "a80acb69c63d5f6437b099c111480a4493bad4592015af2127a2f49fb7512d8d",
+    strip_prefix = "rules_closure-0.7.0",
+    url = "https://github.com/bazelbuild/rules_closure/archive/0.7.0.tar.gz",
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -25,12 +27,12 @@
 http_file(
     name = "polymer_closure",
     sha256 = "5a589bdba674e1fec7188e9251c8624ebf2d4d969beb6635f9148f420d1e08b1",
-    url = "https://raw.githubusercontent.com/google/closure-compiler/775609aad61e14aef289ebec4bfc09ad88877f9e/contrib/externs/polymer-1.0.js",
+    urls = ["https://raw.githubusercontent.com/google/closure-compiler/775609aad61e14aef289ebec4bfc09ad88877f9e/contrib/externs/polymer-1.0.js"],
 )
 
 load("@bazel_skylib//:lib.bzl", "versions")
 
-versions.check(minimum_bazel_version = "0.7.0")
+versions.check(minimum_bazel_version = "0.14.0")
 
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
 
@@ -172,6 +174,26 @@
     sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
 )
 
+FLOGGER_VERS = "0.2"
+
+maven_jar(
+    name = "flogger",
+    artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
+    sha1 = "a22d04ed3b84bae8ecf8aa6d4430ad000bcdf7b4",
+)
+
+maven_jar(
+    name = "flogger-log4j-backend",
+    artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
+    sha1 = "d5085e3996bddc4b105d53b886190cc9a8811a9e",
+)
+
+maven_jar(
+    name = "flogger-system-backend",
+    artifact = "com.google.flogger:flogger-system-backend:" + FLOGGER_VERS,
+    sha1 = "b995c84b8443d6cfbd011a55719b63494b974c3a",
+)
+
 maven_jar(
     name = "gwtjsonrpc",
     artifact = "com.google.gerrit:gwtjsonrpc:1.11",
@@ -181,8 +203,8 @@
 
 maven_jar(
     name = "gson",
-    artifact = "com.google.code.gson:gson:2.8.2",
-    sha1 = "3edcfe49d2c6053a70a2a47e4e1c2f94998a49cf",
+    artifact = "com.google.code.gson:gson:2.8.4",
+    sha1 = "d0de1ca9b69e69d1d497ee3c6009d015f64dad57",
 )
 
 maven_jar(
@@ -194,8 +216,8 @@
 
 maven_jar(
     name = "protobuf",
-    artifact = "com.google.protobuf:protobuf-java:3.4.0",
-    sha1 = "b32aba0cbe737a4ca953f71688725972e3ee927c",
+    artifact = "com.google.protobuf:protobuf-java:3.5.1",
+    sha1 = "8c3492f7662fa1cbf8ca76a0f5eb1146f7725acd",
 )
 
 load("//lib:guava.bzl", "GUAVA_VERSION", "GUAVA_BIN_SHA1")
@@ -233,9 +255,9 @@
 )
 
 maven_jar(
-    name = "log_nop",
-    artifact = "org.slf4j:slf4j-nop:" + SLF4J_VERS,
-    sha1 = "6cca9a3b999ff28b7a35ca762b3197cd7e4c2ad1",
+    name = "log_ext",
+    artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
+    sha1 = "09a8f58c784c37525d2624062414358acf296717",
 )
 
 maven_jar(
@@ -436,7 +458,6 @@
     sha1 = "05b6f921f1810bdf90e25471968f741f87168b64",
 )
 
-# When upgrading Lucene, make sure it's compatible with Elasticsearch
 LUCENE_VERS = "5.5.4"
 
 maven_jar(
@@ -470,42 +491,6 @@
 )
 
 maven_jar(
-    name = "lucene_highlighter",
-    artifact = "org.apache.lucene:lucene-highlighter:" + LUCENE_VERS,
-    sha1 = "433f53f03f1b14337c08d54e507a5410905376fa",
-)
-
-maven_jar(
-    name = "lucene_join",
-    artifact = "org.apache.lucene:lucene-join:" + LUCENE_VERS,
-    sha1 = "23f9a909a244ed3b28b37c5bb21a6e33e6c0a339",
-)
-
-maven_jar(
-    name = "lucene_memory",
-    artifact = "org.apache.lucene:lucene-memory:" + LUCENE_VERS,
-    sha1 = "4dbdc2e1a24837722294762a9edb479f79092ab9",
-)
-
-maven_jar(
-    name = "lucene_spatial",
-    artifact = "org.apache.lucene:lucene-spatial:" + LUCENE_VERS,
-    sha1 = "0217d302dc0ef4d9b8b475ffe327d83c1e0ceba5",
-)
-
-maven_jar(
-    name = "lucene_suggest",
-    artifact = "org.apache.lucene:lucene-suggest:" + LUCENE_VERS,
-    sha1 = "0f46dbb3229eed62dff10d008172c885e0e028c8",
-)
-
-maven_jar(
-    name = "lucene_queries",
-    artifact = "org.apache.lucene:lucene-queries:" + LUCENE_VERS,
-    sha1 = "f915357b8b4b43742ab48f1401dedcaa12dfa37a",
-)
-
-maven_jar(
     name = "mime_util",
     artifact = "eu.medsea.mimeutil:mime-util:2.1.3",
     attach_source = False,
@@ -594,33 +579,33 @@
 )
 
 # When updading Bouncy Castle, also update it in bazlets.
-BC_VERS = "1.57"
+BC_VERS = "1.59"
 
 maven_jar(
     name = "bcprov",
     artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS,
-    sha1 = "f66a135611d42c992e5745788c3f94eb06464537",
+    sha1 = "2507204241ab450456bdb8e8c0a8f986e418bd99",
 )
 
 maven_jar(
     name = "bcpg",
     artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS,
-    sha1 = "7b2d587f5e3780b79e1d35af3e84d00634e9420b",
+    sha1 = "ee93e5376bb6cf0a15c027b5f5e4393f2738e709",
 )
 
 maven_jar(
     name = "bcpkix",
     artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS,
-    sha1 = "5c96e34bc9bd4cd6870e6d193a99438f1e274ca7",
+    sha1 = "9cef0aab8a4bb849a8476c058ce3ff302aba3fff",
 )
 
 # TODO(davido): Remove exlusion of file system provider, when this issue is fixed:
 # https://issues.apache.org/jira/browse/SSHD-736
 maven_jar(
     name = "sshd",
-    artifact = "org.apache.sshd:sshd-core:1.6.0",
+    artifact = "org.apache.sshd:sshd-core:1.7.0",
     exclude = ["META-INF/services/java.nio.file.spi.FileSystemProvider"],
-    sha1 = "548e2da643e88cda9d313efb2564a74f9943e491",
+    sha1 = "2e8b14f6d841b098e46bf407b6fdccab4c19fa41",
 )
 
 maven_jar(
@@ -793,60 +778,60 @@
     sha1 = "df4b50061e8e4c348ce243b921f53ee63ba9bbe1",
 )
 
-JETTY_VERS = "9.3.18.v20170406"
+JETTY_VERS = "9.4.9.v20180320"
 
 maven_jar(
     name = "jetty_servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "534e7fa0e4fb6e08f89eb3f6a8c48b4f81ff5738",
+    sha1 = "d4453b746bc581af6ec5bce09228dc802bec1040",
 )
 
 maven_jar(
     name = "jetty_security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "16b900e91b04511f42b706c925c8af6023d2c05e",
+    sha1 = "dadd28ef757d9b8cdd1d7eef7fcbfa0b482c4648",
 )
 
 maven_jar(
     name = "jetty_servlets",
     artifact = "org.eclipse.jetty:jetty-servlets:" + JETTY_VERS,
-    sha1 = "f9311d1d8e6124d2792f4db5b29514d0ecf46812",
+    sha1 = "cb40696bb683655b7abb4ca72ad08708cd99ca7b",
 )
 
 maven_jar(
     name = "jetty_server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "0a32feea88cba2d43951d22b60861c643454bb3f",
+    sha1 = "08847f7278e8ace7a1f5847e71563c8a10546582",
 )
 
 maven_jar(
     name = "jetty_jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "f988136dc5aa634afed6c5a35d910ee9599c6c23",
+    sha1 = "ff0978e1c74c4e08517df4d1950e61450ea987b1",
 )
 
 maven_jar(
     name = "jetty_continuation",
     artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS,
-    sha1 = "3c5d89c8204d4a48a360087f95e4cbd4520b5de0",
+    sha1 = "590a07c7daf76c755e2daefb1aa0a91b41b26d87",
 )
 
 maven_jar(
     name = "jetty_http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "30ece6d732d276442d513b94d914de6fa1075fae",
+    sha1 = "64d93698196ea7a66b33c754a0eac2a97d5af4b6",
 )
 
 maven_jar(
     name = "jetty_io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "36cb411ee89be1b527b0c10747aa3153267fc3ec",
+    sha1 = "938d67c72405285d2a7a6efb10d870a1b16fa2e0",
 )
 
 maven_jar(
     name = "jetty_util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "8600b7d028a38cb462eff338de91390b3ff5040e",
+    sha1 = "8a602b93581f6af54839728f51d51ab830bdd44d",
 )
 
 maven_jar(
@@ -911,68 +896,10 @@
     sha1 = "8903bf42272062e87a7cbc1d98919e0729a9939f",
 )
 
-# When upgrading Elasticsearch, make sure it's compatible with Lucene
 maven_jar(
-    name = "elasticsearch",
-    artifact = "org.elasticsearch:elasticsearch:2.4.6",
-    sha1 = "d2954e1173a608a9711f132d1768a676a8b1fb81",
-)
-
-# Java REST client for Elasticsearch.
-JEST_VERSION = "2.4.0"
-
-maven_jar(
-    name = "jest_common",
-    artifact = "io.searchbox:jest-common:" + JEST_VERSION,
-    sha1 = "ea779ebe7c438a53dce431f85b0d4e1d8faee2ac",
-)
-
-maven_jar(
-    name = "jest",
-    artifact = "io.searchbox:jest:" + JEST_VERSION,
-    sha1 = "e2a604a584e6633545ac6b1fe99ef888ab96dae9",
-)
-
-maven_jar(
-    name = "joda_time",
-    artifact = "joda-time:joda-time:2.9.9",
-    sha1 = "f7b520c458572890807d143670c9b24f4de90897",
-)
-
-maven_jar(
-    name = "joda_convert",
-    artifact = "org.joda:joda-convert:1.8.1",
-    sha1 = "675642ac208e0b741bc9118dcbcae44c271b992a",
-)
-
-maven_jar(
-    name = "compress_lzf",
-    artifact = "com.ning:compress-lzf:1.0.2",
-    sha1 = "62896e6fca184c79cc01a14d143f3ae2b4f4b4ae",
-)
-
-maven_jar(
-    name = "hppc",
-    artifact = "com.carrotsearch:hppc:0.7.1",
-    sha1 = "8b5057f74ea378c0150a1860874a3ebdcb713767",
-)
-
-maven_jar(
-    name = "jsr166e",
-    artifact = "com.twitter:jsr166e:1.1.0",
-    sha1 = "233098147123ee5ddcd39ffc57ff648be4b7e5b2",
-)
-
-maven_jar(
-    name = "netty",
-    artifact = "io.netty:netty:3.10.0.Final",
-    sha1 = "ad61cd1bba067e6634ddd3e160edf0727391ac30",
-)
-
-maven_jar(
-    name = "t_digest",
-    artifact = "com.tdunning:t-digest:3.0",
-    sha1 = "84ccf145ac2215e6bfa63baa3101c0af41017cfc",
+    name = "elasticsearch-rest-client",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:5.6.9",
+    sha1 = "895706412e2fba3f842fca82ec3dece1cb4ee7d1",
 )
 
 JACKSON_VERSION = "2.8.9"
@@ -984,18 +911,6 @@
 )
 
 maven_jar(
-    name = "jackson_dataformat_cbor",
-    artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:" + JACKSON_VERSION,
-    sha1 = "93242092324cad33d777e06c0515e40a6b862659",
-)
-
-maven_jar(
-    name = "jackson_dataformat_smile",
-    artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:" + JACKSON_VERSION,
-    sha1 = "d36cbae6b06ac12fca16fda403759e479316141b",
-)
-
-maven_jar(
     name = "httpasyncclient",
     artifact = "org.apache.httpcomponents:httpasyncclient:4.1.2",
     sha1 = "95aa3e6fb520191a0970a73cf09f62948ee614be",
@@ -1007,6 +922,30 @@
     sha1 = "a8c5e3c3bfea5ce23fb647c335897e415eb442e3",
 )
 
+maven_jar(
+    name = "testcontainers",
+    artifact = "org.testcontainers:testcontainers:1.7.2",
+    sha1 = "fec8b360b6b613f6c9d3b8e7a9fa32d1a2bcb978",
+)
+
+maven_jar(
+    name = "duct_tape",
+    artifact = "org.rnorth.duct-tape:duct-tape:1.0.7",
+    sha1 = "a26b5d90d88c91321dc7a3734ea72d2fc019ebb6",
+)
+
+maven_jar(
+    name = "visible_assertions",
+    artifact = "org.rnorth.visible-assertions:visible-assertions:2.1.0",
+    sha1 = "f2fcff2862860828ac38a5e1f14d941787c06b13",
+)
+
+maven_jar(
+    name = "jna",
+    artifact = "net.java.dev.jna:jna:4.5.1",
+    sha1 = "65bd0cacc9c79a21c6ed8e9f588577cd3c2f85b9",
+)
+
 load("//tools/bzl:js.bzl", "npm_binary", "bower_archive")
 
 npm_binary(
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
index 07a0f01..22e0c1b 100755
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -275,40 +275,6 @@
 
 
 def main():
-<<<<<<< HEAD
-  p = optparse.OptionParser()
-  p.add_option("-u", "--user_count", action="store",
-               default=100,
-               type='int',
-               help="number of users to generate")
-  p.add_option("-p", "--port", action="store",
-               default=8080,
-               type='int',
-               help="port of server")
-  (options, _) = p.parse_args()
-  global BASE_URL
-  BASE_URL = BASE_URL % options.port
-  print(BASE_URL)
-
-  set_up()
-  gerrit_users = get_random_users(options.user_count)
-
-  group_names = create_gerrit_groups()
-  for idx, u in enumerate(gerrit_users):
-    u["groups"].append(group_names[idx % len(group_names)])
-    if idx % 5 == 0:
-      # Also add to security group
-      u["groups"].append(group_names[4])
-
-  generate_ssh_keys(gerrit_users)
-  create_gerrit_users(gerrit_users)
-
-  project_names = create_gerrit_projects(group_names)
-
-  for idx, u in enumerate(gerrit_users):
-    for _ in range(random.randint(1, 5)):
-      create_change(u, project_names[4 * idx / len(gerrit_users)])
-=======
     p = optparse.OptionParser()
     p.add_option("-u", "--user_count", action="store",
                  default=100,
@@ -342,6 +308,4 @@
         for _ in xrange(random.randint(1, 5)):
             create_change(u, project_names[4 * idx / len(gerrit_users)])
 
->>>>>>> 730efd14f4... Python cleanups, round 1: whitespace
-
 main()
diff --git a/gerrit-gwtdebug/BUILD b/gerrit-gwtdebug/BUILD
index b4cd663..f564745 100644
--- a/gerrit-gwtdebug/BUILD
+++ b/gerrit-gwtdebug/BUILD
@@ -6,11 +6,11 @@
         "//java/com/google/gerrit/pgm",
         "//java/com/google/gerrit/pgm/util",
         "//java/com/google/gerrit/util/cli",
+        "//lib/flogger:api",
         "//lib/gwt:dev",
         "//lib/jetty:server",
         "//lib/jetty:servlet",
         "//lib/jetty:servlets",
-        "//lib/log:api",
         "//lib/log:log4j",
     ],
 )
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
index 4edff0e..cf84919 100644
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
+++ b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
@@ -14,16 +14,15 @@
 
 package com.google.gerrit.gwtdebug;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.pgm.Daemon;
 import com.google.gwt.dev.codeserver.CodeServer;
 import com.google.gwt.dev.codeserver.Options;
 import java.util.ArrayList;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class GerritGwtDebugLauncher {
-  private static final Logger log = LoggerFactory.getLogger(GerritGwtDebugLauncher.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static void main(String[] argv) throws Exception {
     GerritGwtDebugLauncher launcher = new GerritGwtDebugLauncher();
@@ -54,7 +53,7 @@
 
     Options options = new Options();
     if (!options.parseArgs(sdmLauncherOptions.toArray(new String[sdmLauncherOptions.size()]))) {
-      log.error("Failed to parse codeserver arguments");
+      logger.atSevere().log("Failed to parse codeserver arguments");
       return 1;
     }
 
@@ -65,11 +64,11 @@
           new Daemon()
               .main(daemonLauncherOptions.toArray(new String[daemonLauncherOptions.size()]));
       if (r != 0) {
-        log.error("Daemon exited with return code: " + r);
+        logger.atSevere().log("Daemon exited with return code: %d", r);
         return 1;
       }
     } catch (Exception e) {
-      log.error("Cannot start daemon", e);
+      logger.atSevere().withCause(e).log("Cannot start daemon");
       return 1;
     }
 
diff --git a/java/Main.java b/java/Main.java
index 0eca665..f26b6df 100644
--- a/java/Main.java
+++ b/java/Main.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 
 public final class Main {
+  private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
+
   // We don't do any real work here because we need to import
   // the archive lookup code and we cannot import a class in
   // the default package. So this is just a tiny springboard
@@ -21,6 +23,7 @@
 
   public static void main(String[] argv) throws Exception {
     if (onSupportedJavaVersion()) {
+      configureFloggerBackend();
       com.google.gerrit.launcher.GerritLauncher.main(argv);
 
     } else {
@@ -38,6 +41,18 @@
     return false;
   }
 
+  private static void configureFloggerBackend() {
+    if (System.getProperty(FLOGGER_BACKEND_PROPERTY) != null) {
+      // Flogger backend is already configured
+      return;
+    }
+
+    // Configure log4j backend
+    System.setProperty(
+        FLOGGER_BACKEND_PROPERTY,
+        "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");
+  }
+
   private static double parse(String version) {
     if (version == null || version.length() == 0) {
       return 0.0;
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 9587860..770805b 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -42,11 +42,11 @@
         "//lib/bouncycastle:bcpg",
         "//lib/bouncycastle:bcprov",
         "//lib/commons:compress",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/mina:sshd",
         "//prolog:gerrit-prolog-common",
     ],
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 5699e3f..6e5424c 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.Daemon;
 import com.google.gerrit.pgm.Init;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
@@ -44,6 +45,7 @@
 import com.google.gerrit.testing.NoteDbMode;
 import com.google.gerrit.testing.SshMode;
 import com.google.gerrit.testing.TempFileUtil;
+import com.google.inject.AbstractModule;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
@@ -352,7 +354,13 @@
     daemon.setDatabaseForTesting(
         ImmutableList.<Module>of(
             new InMemoryTestingDatabaseModule(
-                cfg, site, inMemoryRepoManager, inMemoryDatabaseInstance)));
+                cfg, site, inMemoryRepoManager, inMemoryDatabaseInstance),
+            new AbstractModule() {
+              @Override
+              protected void configure() {
+                bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
+              }
+            }));
     daemon.start();
     return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
   }
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
similarity index 97%
rename from lib/asciidoctor/java/AsciiDoctor.java
rename to java/com/google/gerrit/asciidoctor/AsciiDoctor.java
index 596fe66..8b432ff 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package com.google.gerrit.asciidoctor;
+
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.ByteStreams;
@@ -70,9 +72,8 @@
   private List<String> attributes = new ArrayList<>();
 
   @Option(
-    name = "--bazel",
-    usage = "bazel mode: generate multiple output files instead of a single zip file"
-  )
+      name = "--bazel",
+      usage = "bazel mode: generate multiple output files instead of a single zip file")
   private boolean bazel;
 
   @Option(name = "--revnumber-file", usage = "the file contains revnumber string")
diff --git a/java/com/google/gerrit/asciidoctor/BUILD b/java/com/google/gerrit/asciidoctor/BUILD
new file mode 100644
index 0000000..f5178a0
--- /dev/null
+++ b/java/com/google/gerrit/asciidoctor/BUILD
@@ -0,0 +1,38 @@
+java_binary(
+    name = "asciidoc",
+    main_class = "com.google.gerrit.asciidoctor.AsciiDoctor",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":asciidoc_lib"],
+)
+
+java_library(
+    name = "asciidoc_lib",
+    srcs = ["AsciiDoctor.java"],
+    visibility = ["//tools/eclipse:__pkg__"],
+    deps = [
+        "//lib:args4j",
+        "//lib:guava",
+        "//lib/asciidoctor",
+    ],
+)
+
+java_binary(
+    name = "doc_indexer",
+    main_class = "com.google.gerrit.asciidoctor.DocIndexer",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":doc_indexer_lib"],
+)
+
+java_library(
+    name = "doc_indexer_lib",
+    srcs = ["DocIndexer.java"],
+    visibility = ["//tools/eclipse:__pkg__"],
+    deps = [
+        ":asciidoc_lib",
+        "//java/com/google/gerrit/server:constants",
+        "//lib:args4j",
+        "//lib:guava",
+        "//lib/lucene:lucene-analyzers-common",
+        "//lib/lucene:lucene-core-and-backward-codecs",
+    ],
+)
diff --git a/lib/asciidoctor/java/DocIndexer.java b/java/com/google/gerrit/asciidoctor/DocIndexer.java
similarity index 98%
rename from lib/asciidoctor/java/DocIndexer.java
rename to java/com/google/gerrit/asciidoctor/DocIndexer.java
index c90c439..5dfde95 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/java/com/google/gerrit/asciidoctor/DocIndexer.java
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package com.google.gerrit.asciidoctor;
+
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.server.documentation.Constants;
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 2565f0d..800a975 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -24,8 +24,8 @@
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
     gwt_xml = "Common.gwt.xml",
     visibility = ["//visibility:public"],
@@ -50,8 +50,8 @@
         "//lib:servlet-api-3_1",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
 
diff --git a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index e8fa896..cf86f74 100644
--- a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common;
 
+import static com.google.common.flogger.LazyArgs.lazy;
 import static com.google.gerrit.common.FileUtil.lastModified;
 import static java.util.stream.Collectors.joining;
 
@@ -21,26 +22,25 @@
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @GwtIncompatible("Unemulated classes in java.nio and Guava")
 public final class SiteLibraryLoaderUtil {
-  private static final Logger log = LoggerFactory.getLogger(SiteLibraryLoaderUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static void loadSiteLib(Path libdir) {
     try {
       List<Path> jars = listJars(libdir);
       IoUtil.loadJARs(jars);
-      log.debug("Loaded site libraries: {}", jarList(jars));
+      logger.atFine().log("Loaded site libraries: %s", lazy(() -> jarList(jars)));
     } catch (IOException e) {
-      log.error("Error scanning lib directory " + libdir, e);
+      logger.atSevere().withCause(e).log("Error scanning lib directory %s", libdir);
     }
   }
 
diff --git a/java/com/google/gerrit/common/TimeUtil.java b/java/com/google/gerrit/common/TimeUtil.java
index 7f53f84..e42eb09 100644
--- a/java/com/google/gerrit/common/TimeUtil.java
+++ b/java/com/google/gerrit/common/TimeUtil.java
@@ -17,6 +17,7 @@
 import com.google.common.annotations.GwtIncompatible;
 import com.google.common.annotations.VisibleForTesting;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.function.LongSupplier;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -36,6 +37,10 @@
     return currentMillisSupplier.getAsLong();
   }
 
+  public static Instant now() {
+    return Instant.ofEpochMilli(nowMs());
+  }
+
   public static Timestamp nowTs() {
     return new Timestamp(nowMs());
   }
diff --git a/java/com/google/gerrit/common/Version.java b/java/com/google/gerrit/common/Version.java
index 1777c3c..b8d3b67 100644
--- a/java/com/google/gerrit/common/Version.java
+++ b/java/com/google/gerrit/common/Version.java
@@ -18,16 +18,15 @@
 
 import com.google.common.annotations.GwtIncompatible;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @GwtIncompatible("Unemulated com.google.gerrit.common.Version")
 public class Version {
-  private static final Logger log = LoggerFactory.getLogger(Version.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @VisibleForTesting static final String DEV = "(dev)";
 
@@ -57,7 +56,7 @@
         return vs;
       }
     } catch (IOException e) {
-      log.error(e.getMessage(), e);
+      logger.atSevere().withCause(e).log(e.getMessage());
       return "(unknown version)";
     }
   }
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 107fb0f..0ccd820 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -16,29 +16,28 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
-import static java.util.stream.Collectors.toList;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.commons.codec.binary.Base64.decodeBase64;
-import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.io.CharStreams;
+import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.elasticsearch.builders.QueryBuilder;
+import com.google.gerrit.elasticsearch.builders.SearchSourceBuilder;
+import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
-import com.google.gerrit.index.Schema.Values;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gson.Gson;
@@ -46,38 +45,40 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
-import io.searchbox.client.JestResult;
-import io.searchbox.client.http.JestHttpClient;
-import io.searchbox.core.Bulk;
-import io.searchbox.core.Delete;
-import io.searchbox.core.Search;
-import io.searchbox.core.search.sort.Sort;
-import io.searchbox.indices.CreateIndex;
-import io.searchbox.indices.DeleteIndex;
-import io.searchbox.indices.IndicesExists;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
 import java.sql.Timestamp;
-import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Function;
 import org.apache.commons.codec.binary.Base64;
-import org.eclipse.jgit.lib.Config;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.builder.SearchSourceBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.nio.entity.NStringEntity;
+import org.elasticsearch.client.Response;
 
 abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
-  private static final Logger log = LoggerFactory.getLogger(AbstractElasticIndex.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  protected static final String BULK = "_bulk";
+  protected static final String MAPPINGS = "mappings";
+  protected static final String ORDER = "order";
+  protected static final String SEARCH = "_search";
 
   protected static <T> List<T> decodeProtos(
       JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
@@ -90,33 +91,42 @@
         .toList();
   }
 
+  static String getContent(Response response) throws IOException {
+    HttpEntity responseEntity = response.getEntity();
+    String content = "";
+    if (responseEntity != null) {
+      InputStream contentStream = responseEntity.getContent();
+      try (Reader reader = new InputStreamReader(contentStream)) {
+        content = CharStreams.toString(reader);
+      }
+    }
+    return content;
+  }
+
   private final Schema<V> schema;
   private final SitePaths sitePaths;
   private final String indexNameRaw;
 
+  protected final String type;
+  protected final ElasticRestClientProvider client;
   protected final String indexName;
-  protected final JestHttpClient client;
   protected final Gson gson;
   protected final ElasticQueryBuilder queryBuilder;
 
   AbstractElasticIndex(
-      @GerritServerConfig Config cfg,
+      ElasticConfiguration cfg,
       SitePaths sitePaths,
       Schema<V> schema,
-      JestClientBuilder clientBuilder,
+      ElasticRestClientProvider client,
       String indexName) {
     this.sitePaths = sitePaths;
     this.schema = schema;
     this.gson = new GsonBuilder().setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
     this.queryBuilder = new ElasticQueryBuilder();
-    this.indexName =
-        String.format(
-            "%s%s_%04d",
-            Strings.nullToEmpty(cfg.getString("elasticsearch", null, "prefix")),
-            indexName,
-            schema.getVersion());
+    this.indexName = cfg.getIndexName(indexName, schema.getVersion());
     this.indexNameRaw = indexName;
-    this.client = clientBuilder.build();
+    this.client = client;
+    this.type = client.adapter().getType(indexName);
   }
 
   @Override
@@ -126,7 +136,7 @@
 
   @Override
   public void close() {
-    client.shutdownClient();
+    // Do nothing. Client is closed by the provider.
   }
 
   @Override
@@ -135,78 +145,60 @@
   }
 
   @Override
-  public void delete(K c) throws IOException {
-    Bulk bulk = addActions(new Bulk.Builder(), c).refresh(true).build();
-    JestResult result = client.execute(bulk);
-    if (!result.isSucceeded()) {
+  public void delete(K id) throws IOException {
+    String uri = getURI(type, BULK);
+    Response response = postRequest(getDeleteActions(id), uri, getRefreshParam());
+    int statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode != HttpStatus.SC_OK) {
       throw new IOException(
-          String.format(
-              "Failed to delete change %s in index %s: %s",
-              c, indexName, result.getErrorMessage()));
+          String.format("Failed to delete %s from index %s: %s", id, indexName, statusCode));
     }
   }
 
   @Override
   public void deleteAll() throws IOException {
     // Delete the index, if it exists.
-    JestResult result = client.execute(new IndicesExists.Builder(indexName).build());
-    if (result.isSucceeded()) {
-      result = client.execute(new DeleteIndex.Builder(indexName).build());
-      if (!result.isSucceeded()) {
+    String endpoint = indexName + client.adapter().indicesExistParam();
+    Response response = client.get().performRequest("HEAD", endpoint);
+    int statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode == HttpStatus.SC_OK) {
+      response = client.get().performRequest("DELETE", indexName);
+      statusCode = response.getStatusLine().getStatusCode();
+      if (statusCode != HttpStatus.SC_OK) {
         throw new IOException(
-            String.format("Failed to delete index %s: %s", indexName, result.getErrorMessage()));
+            String.format("Failed to delete index %s: %s", indexName, statusCode));
       }
     }
 
     // Recreate the index.
-    result = client.execute(new CreateIndex.Builder(indexName).settings(getMappings()).build());
-    if (!result.isSucceeded()) {
-      String error =
-          String.format("Failed to create index %s: %s", indexName, result.getErrorMessage());
+    response = performRequest("PUT", getMappings(), indexName, Collections.emptyMap());
+    statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode != HttpStatus.SC_OK) {
+      String error = String.format("Failed to create index %s: %s", indexName, statusCode);
       throw new IOException(error);
     }
   }
 
-  protected abstract Bulk.Builder addActions(Bulk.Builder builder, K c);
+  protected abstract String getDeleteActions(K id);
 
   protected abstract String getMappings();
 
   protected abstract String getId(V v);
 
-  protected Delete delete(String type, K c) {
-    String id = c.toString();
-    return new Delete.Builder(id).index(indexName).type(type).build();
+  protected String getMappingsForSingleType(String candidateType, MappingProperties properties) {
+    return getMappingsFor(client.adapter().getType(candidateType), properties);
   }
 
-  protected io.searchbox.core.Index insert(String type, V v) throws IOException {
-    String id = getId(v);
-    String doc = toDocument(v);
-    return new io.searchbox.core.Index.Builder(doc).index(indexName).type(type).id(id).build();
+  protected String getMappingsFor(String type, MappingProperties properties) {
+    JsonObject mappingType = new JsonObject();
+    mappingType.add(type, gson.toJsonTree(properties));
+    JsonObject mappings = new JsonObject();
+    mappings.add(MAPPINGS, gson.toJsonTree(mappingType));
+    return gson.toJson(mappings);
   }
 
-  private static boolean shouldAddElement(Object element) {
-    return !(element instanceof String) || !((String) element).isEmpty();
-  }
-
-  private String toDocument(V v) throws IOException {
-    try (XContentBuilder builder = jsonBuilder().startObject()) {
-      for (Values<V> values : schema.buildFields(v)) {
-        String name = values.getField().getName();
-        if (values.getField().isRepeatable()) {
-          builder.field(
-              name,
-              Streams.stream(values.getValues())
-                  .filter(AbstractElasticIndex::shouldAddElement)
-                  .collect(toList()));
-        } else {
-          Object element = Iterables.getOnlyElement(values.getValues(), "");
-          if (shouldAddElement(element)) {
-            builder.field(name, element);
-          }
-        }
-      }
-      return builder.endObject().string();
-    }
+  protected String delete(String type, K id) {
+    return new DeleteRequest(id.toString(), indexName, type, client.adapter()).toString();
   }
 
   protected abstract V fromDocument(JsonObject doc, Set<String> fields);
@@ -214,16 +206,15 @@
   protected FieldBundle toFieldBundle(JsonObject doc) {
     Map<String, FieldDef<V, ?>> allFields = getSchema().getFields();
     ListMultimap<String, Object> rawFields = ArrayListMultimap.create();
-    for (Entry<String, JsonElement> element : doc.get("fields").getAsJsonObject().entrySet()) {
+    for (Map.Entry<String, JsonElement> element :
+        doc.get(client.adapter().rawFieldsKey()).getAsJsonObject().entrySet()) {
       checkArgument(
           allFields.containsKey(element.getKey()), "Unrecognized field " + element.getKey());
       FieldType<?> type = allFields.get(element.getKey()).getType();
-
       Iterable<JsonElement> innerItems =
           element.getValue().isJsonArray()
               ? element.getValue().getAsJsonArray()
               : Collections.singleton(element.getValue());
-
       for (JsonElement inner : innerItems) {
         if (type == FieldType.EXACT || type == FieldType.FULL_TEXT || type == FieldType.PREFIX) {
           rawFields.put(element.getKey(), inner.getAsString());
@@ -243,33 +234,80 @@
     return new FieldBundle(rawFields);
   }
 
+  protected String toAction(String type, String id, String action) {
+    JsonObject properties = new JsonObject();
+    properties.addProperty("_id", id);
+    properties.addProperty("_index", indexName);
+    properties.addProperty("_type", type);
+
+    JsonObject jsonAction = new JsonObject();
+    jsonAction.add(action, properties);
+    return jsonAction.toString() + System.lineSeparator();
+  }
+
+  protected void addNamedElement(String name, JsonObject element, JsonArray array) {
+    JsonObject arrayElement = new JsonObject();
+    arrayElement.add(name, element);
+    array.add(arrayElement);
+  }
+
+  protected Map<String, String> getRefreshParam() {
+    Map<String, String> params = new HashMap<>();
+    params.put("refresh", "true");
+    return params;
+  }
+
+  protected String getSearch(SearchSourceBuilder searchSource, JsonArray sortArray) {
+    JsonObject search = new JsonParser().parse(searchSource.toString()).getAsJsonObject();
+    search.add("sort", sortArray);
+    return gson.toJson(search);
+  }
+
+  protected JsonArray getSortArray(String idFieldName) {
+    JsonObject properties = new JsonObject();
+    properties.addProperty(ORDER, "asc");
+    client.adapter().setIgnoreUnmapped(properties);
+
+    JsonArray sortArray = new JsonArray();
+    addNamedElement(idFieldName, properties, sortArray);
+    return sortArray;
+  }
+
+  protected String getURI(String type, String request) throws UnsupportedEncodingException {
+    String encodedType = URLEncoder.encode(type, UTF_8.toString());
+    String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
+    return encodedIndexName + "/" + encodedType + "/" + request;
+  }
+
+  protected Response postRequest(Object payload, String uri, Map<String, String> params)
+      throws IOException {
+    return performRequest("POST", payload, uri, params);
+  }
+
+  private Response performRequest(
+      String method, Object payload, String uri, Map<String, String> params) throws IOException {
+    String payloadStr = payload instanceof String ? (String) payload : payload.toString();
+    HttpEntity entity = new NStringEntity(payloadStr, ContentType.APPLICATION_JSON);
+    return client.get().performRequest(method, uri, params, entity);
+  }
+
   protected class ElasticQuerySource implements DataSource<V> {
     private final QueryOptions opts;
-    private final Search search;
+    private final String search;
+    private final String index;
 
-    ElasticQuerySource(Predicate<V> p, QueryOptions opts, String type, Sort sort)
-        throws QueryParseException {
-      this(p, opts, ImmutableList.of(type), ImmutableList.of(sort));
-    }
-
-    ElasticQuerySource(
-        Predicate<V> p, QueryOptions opts, Collection<String> types, Collection<Sort> sorts)
+    ElasticQuerySource(Predicate<V> p, QueryOptions opts, String index, JsonArray sortArray)
         throws QueryParseException {
       this.opts = opts;
+      this.index = index;
       QueryBuilder qb = queryBuilder.toQueryBuilder(p);
       SearchSourceBuilder searchSource =
-          new SearchSourceBuilder()
+          new SearchSourceBuilder(client.adapter())
               .query(qb)
               .from(opts.start())
               .size(opts.limit())
               .fields(Lists.newArrayList(opts.fields()));
-
-      search =
-          new Search.Builder(searchSource.toString())
-              .addType(types)
-              .addSort(sorts)
-              .addIndex(indexName)
-              .build();
+      search = getSearch(searchSource, sortArray);
     }
 
     @Override
@@ -287,17 +325,17 @@
       return readImpl(AbstractElasticIndex.this::toFieldBundle);
     }
 
-    @Override
-    public String toString() {
-      return search.toString();
-    }
-
     private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) throws OrmException {
       try {
         List<T> results = Collections.emptyList();
-        JestResult result = client.execute(search);
-        if (result.isSucceeded()) {
-          JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
+        String uri = getURI(index, SEARCH);
+        Response response =
+            performRequest(HttpPost.METHOD_NAME, search, uri, Collections.emptyMap());
+        StatusLine statusLine = response.getStatusLine();
+        if (statusLine.getStatusCode() == HttpStatus.SC_OK) {
+          String content = getContent(response);
+          JsonObject obj =
+              new JsonParser().parse(content).getAsJsonObject().getAsJsonObject("hits");
           if (obj.get("hits") != null) {
             JsonArray json = obj.getAsJsonArray("hits");
             results = Lists.newArrayListWithCapacity(json.size());
@@ -309,7 +347,7 @@
             }
           }
         } else {
-          log.error(result.getErrorMessage());
+          logger.atSevere().log(statusLine.getReasonPhrase());
         }
         final List<T> r = Collections.unmodifiableList(results);
         return new ResultSet<T>() {
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index f5ada85..31ede79 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -16,15 +16,15 @@
         "//lib:protobuf",
         "//lib/commons:codec",
         "//lib/commons:lang",
-        "//lib/elasticsearch",
-        "//lib/elasticsearch:joda-time",
+        "//lib/elasticsearch-rest-client",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
-        "//lib/jest",
-        "//lib/jest:jest-common",
+        "//lib/httpcomponents:httpasyncclient",
+        "//lib/httpcomponents:httpclient",
+        "//lib/httpcomponents:httpcore",
+        "//lib/httpcomponents:httpcore-nio",
+        "//lib/jackson:jackson-core",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
-        "//lib/lucene:lucene-analyzers-common",
-        "//lib/lucene:lucene-core",
     ],
 )
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index 8ac0109..58f4fb9 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -16,8 +16,10 @@
 
 import static com.google.gerrit.server.index.account.AccountField.ID;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.elasticsearch.bulk.BulkRequest;
+import com.google.gerrit.elasticsearch.bulk.IndexRequest;
+import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -26,32 +28,28 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import io.searchbox.client.JestResult;
-import io.searchbox.core.Bulk;
-import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.search.sort.Sort;
-import io.searchbox.core.search.sort.Sort.Sorting;
 import java.io.IOException;
 import java.util.Set;
-import org.eclipse.jgit.lib.Config;
+import org.apache.http.HttpStatus;
+import org.elasticsearch.client.Response;
 
 public class ElasticAccountIndex extends AbstractElasticIndex<Account.Id, AccountState>
     implements AccountIndex {
   static class AccountMapping {
     MappingProperties accounts;
 
-    AccountMapping(Schema<AccountState> schema) {
-      this.accounts = ElasticMapping.createMapping(schema);
+    AccountMapping(Schema<AccountState> schema, ElasticQueryAdapter adapter) {
+      this.accounts = ElasticMapping.createMapping(schema, adapter);
     }
   }
 
@@ -59,54 +57,54 @@
 
   private final AccountMapping mapping;
   private final Provider<AccountCache> accountCache;
+  private final Schema<AccountState> schema;
 
   @Inject
   ElasticAccountIndex(
-      @GerritServerConfig Config cfg,
+      ElasticConfiguration cfg,
       SitePaths sitePaths,
       Provider<AccountCache> accountCache,
-      JestClientBuilder clientBuilder,
+      ElasticRestClientProvider client,
       @Assisted Schema<AccountState> schema) {
-    super(cfg, sitePaths, schema, clientBuilder, ACCOUNTS);
+    super(cfg, sitePaths, schema, client, ACCOUNTS);
     this.accountCache = accountCache;
-    this.mapping = new AccountMapping(schema);
+    this.mapping = new AccountMapping(schema, client.adapter());
+    this.schema = schema;
   }
 
   @Override
   public void replace(AccountState as) throws IOException {
-    Bulk bulk =
-        new Bulk.Builder()
-            .defaultIndex(indexName)
-            .defaultType(ACCOUNTS)
-            .addAction(insert(ACCOUNTS, as))
-            .refresh(true)
-            .build();
-    JestResult result = client.execute(bulk);
-    if (!result.isSucceeded()) {
+    BulkRequest bulk =
+        new IndexRequest(getId(as), indexName, type, client.adapter())
+            .add(new UpdateRequest<>(schema, as));
+
+    String uri = getURI(type, BULK);
+    Response response = postRequest(bulk, uri, getRefreshParam());
+    int statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode != HttpStatus.SC_OK) {
       throw new IOException(
           String.format(
               "Failed to replace account %s in index %s: %s",
-              as.getAccount().getId(), indexName, result.getErrorMessage()));
+              as.getAccount().getId(), indexName, statusCode));
     }
   }
 
   @Override
   public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
       throws QueryParseException {
-    Sort sort = new Sort(AccountField.ID.getName(), Sorting.ASC);
-    sort.setIgnoreUnmapped();
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::accountFields), ACCOUNTS, sort);
+    JsonArray sortArray = getSortArray(AccountField.ID.getName());
+    return new ElasticQuerySource(
+        p, opts.filterFields(IndexUtils::accountFields), ACCOUNTS, sortArray);
   }
 
   @Override
-  protected Builder addActions(Builder builder, Account.Id c) {
-    return builder.addAction(delete(ACCOUNTS, c));
+  protected String getDeleteActions(Account.Id a) {
+    return delete(type, a);
   }
 
   @Override
   protected String getMappings() {
-    ImmutableMap<String, AccountMapping> mappings = ImmutableMap.of("mappings", mapping);
-    return gson.toJson(mappings);
+    return getMappingsForSingleType(ACCOUNTS, mapping.accounts);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 58a298e..1ec8d2b 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -24,7 +24,6 @@
 import static org.apache.commons.codec.binary.Base64.decodeBase64;
 
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -33,6 +32,10 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.elasticsearch.bulk.BulkRequest;
+import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
+import com.google.gerrit.elasticsearch.bulk.IndexRequest;
+import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -46,7 +49,6 @@
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -61,28 +63,26 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import io.searchbox.client.JestResult;
-import io.searchbox.core.Bulk;
-import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.search.sort.Sort;
-import io.searchbox.core.search.sort.Sort.Sorting;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.codec.binary.Base64;
-import org.eclipse.jgit.lib.Config;
+import org.apache.http.HttpStatus;
+import org.elasticsearch.client.Response;
 
 /** Secondary index implementation using Elasticsearch. */
 class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
     implements ChangeIndex {
   static class ChangeMapping {
+    MappingProperties changes;
     MappingProperties openChanges;
     MappingProperties closedChanges;
 
-    ChangeMapping(Schema<ChangeData> schema) {
-      MappingProperties mapping = ElasticMapping.createMapping(schema);
+    ChangeMapping(Schema<ChangeData> schema, ElasticQueryAdapter adapter) {
+      MappingProperties mapping = ElasticMapping.createMapping(schema, adapter);
+      this.changes = mapping;
       this.openChanges = mapping;
       this.closedChanges = mapping;
     }
@@ -95,19 +95,21 @@
   private final ChangeMapping mapping;
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
+  private final Schema<ChangeData> schema;
 
   @Inject
   ElasticChangeIndex(
-      @GerritServerConfig Config cfg,
+      ElasticConfiguration cfg,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       SitePaths sitePaths,
-      JestClientBuilder clientBuilder,
+      ElasticRestClientProvider clientBuilder,
       @Assisted Schema<ChangeData> schema) {
     super(cfg, sitePaths, schema, clientBuilder, CHANGES);
     this.db = db;
     this.changeDataFactory = changeDataFactory;
-    mapping = new ChangeMapping(schema);
+    this.schema = schema;
+    mapping = new ChangeMapping(schema, client.adapter());
   }
 
   @Override
@@ -127,20 +129,21 @@
       throw new IOException(e);
     }
 
-    Bulk bulk =
-        new Bulk.Builder()
-            .defaultIndex(indexName)
-            .defaultType("changes")
-            .addAction(insert(insertIndex, cd))
-            .addAction(delete(deleteIndex, cd.getId()))
-            .refresh(true)
-            .build();
-    JestResult result = client.execute(bulk);
-    if (!result.isSucceeded()) {
+    ElasticQueryAdapter adapter = client.adapter();
+    BulkRequest bulk =
+        new IndexRequest(getId(cd), indexName, adapter.getType(insertIndex), adapter)
+            .add(new UpdateRequest<>(schema, cd));
+    if (!adapter.usePostV5Type()) {
+      bulk.add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex, adapter));
+    }
+
+    String uri = getURI(type, BULK);
+    Response response = postRequest(bulk, uri, getRefreshParam());
+    int statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode != HttpStatus.SC_OK) {
       throw new IOException(
           String.format(
-              "Failed to replace change %s in index %s: %s",
-              cd.getId(), indexName, result.getErrorMessage()));
+              "Failed to replace change %s in index %s: %s", cd.getId(), indexName, statusCode));
     }
   }
 
@@ -149,32 +152,53 @@
       throws QueryParseException {
     Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
     List<String> indexes = Lists.newArrayListWithCapacity(2);
-    if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
-      indexes.add(OPEN_CHANGES);
-    }
-    if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
-      indexes.add(CLOSED_CHANGES);
+    if (client.adapter().usePostV5Type()) {
+      if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()
+          || !Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+        indexes.add(ElasticQueryAdapter.POST_V5_TYPE);
+      }
+    } else {
+      if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+        indexes.add(OPEN_CHANGES);
+      }
+      if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+        indexes.add(CLOSED_CHANGES);
+      }
     }
 
-    List<Sort> sorts =
-        ImmutableList.of(
-            new Sort(ChangeField.UPDATED.getName(), Sorting.DESC),
-            new Sort(ChangeField.LEGACY_ID.getName(), Sorting.DESC));
-    for (Sort sort : sorts) {
-      sort.setIgnoreUnmapped();
-    }
     QueryOptions filteredOpts = opts.filterFields(IndexUtils::changeFields);
-    return new ElasticQuerySource(p, filteredOpts, indexes, sorts);
+    return new ElasticQuerySource(p, filteredOpts, getURI(indexes), getSortArray());
+  }
+
+  private JsonArray getSortArray() {
+    JsonObject properties = new JsonObject();
+    properties.addProperty(ORDER, "desc");
+    client.adapter().setIgnoreUnmapped(properties);
+
+    JsonArray sortArray = new JsonArray();
+    addNamedElement(ChangeField.UPDATED.getName(), properties, sortArray);
+    addNamedElement(ChangeField.LEGACY_ID.getName(), properties, sortArray);
+    return sortArray;
+  }
+
+  private String getURI(List<String> types) {
+    return String.join(",", types);
   }
 
   @Override
-  protected Builder addActions(Builder builder, Id c) {
-    return builder.addAction(delete(OPEN_CHANGES, c)).addAction(delete(OPEN_CHANGES, c));
+  protected String getDeleteActions(Id c) {
+    if (client.adapter().usePostV5Type()) {
+      return delete(ElasticQueryAdapter.POST_V5_TYPE, c);
+    }
+    return delete(OPEN_CHANGES, c) + delete(CLOSED_CHANGES, c);
   }
 
   @Override
   protected String getMappings() {
-    return gson.toJson(ImmutableMap.of("mappings", mapping));
+    if (client.adapter().usePostV5Type()) {
+      return getMappingsFor(ElasticQueryAdapter.POST_V5_TYPE, mapping.changes);
+    }
+    return gson.toJson(ImmutableMap.of(MAPPINGS, mapping));
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
index 7ae49c7..84dae7f 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.elasticsearch;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import org.apache.http.HttpHost;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -33,7 +33,9 @@
   private static final String DEFAULT_PORT = "9200";
   private static final String DEFAULT_PROTOCOL = "http";
 
-  final List<String> urls;
+  private final Config cfg;
+
+  final List<HttpHost> urls;
   final String username;
   final String password;
   final boolean requestCompression;
@@ -42,9 +44,11 @@
   final TimeUnit maxConnectionIdleUnit = TimeUnit.MILLISECONDS;
   final int maxTotalConnection;
   final int readTimeout;
+  final String prefix;
 
   @Inject
   ElasticConfiguration(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
     this.username = cfg.getString("elasticsearch", null, "username");
     this.password = cfg.getString("elasticsearch", null, "password");
     this.requestCompression = cfg.getBoolean("elasticsearch", null, "requestCompression", false);
@@ -56,37 +60,35 @@
     this.maxTotalConnection = cfg.getInt("elasticsearch", null, "maxTotalConnection", 1);
     this.readTimeout =
         (int) cfg.getTimeUnit("elasticsearch", null, "readTimeout", 3000, TimeUnit.MICROSECONDS);
+    this.prefix = Strings.nullToEmpty(cfg.getString("elasticsearch", null, "prefix"));
 
     Set<String> subsections = cfg.getSubsections("elasticsearch");
     if (subsections.isEmpty()) {
-      this.urls = Arrays.asList(buildUrl(DEFAULT_PROTOCOL, DEFAULT_HOST, DEFAULT_PORT));
+      HttpHost httpHost =
+          new HttpHost(DEFAULT_HOST, Integer.valueOf(DEFAULT_PORT), DEFAULT_PROTOCOL);
+      this.urls = Collections.singletonList(httpHost);
     } else {
       this.urls = new ArrayList<>(subsections.size());
       for (String subsection : subsections) {
         String port = getString(cfg, subsection, "port", DEFAULT_PORT);
         String host = getString(cfg, subsection, "hostname", DEFAULT_HOST);
         String protocol = getString(cfg, subsection, "protocol", DEFAULT_PROTOCOL);
-        this.urls.add(buildUrl(protocol, host, port));
+
+        HttpHost httpHost = new HttpHost(host, Integer.valueOf(port), protocol);
+        this.urls.add(httpHost);
       }
     }
   }
 
-  private String getString(Config cfg, String subsection, String name, String defaultValue) {
-    return MoreObjects.firstNonNull(cfg.getString("elasticsearch", subsection, name), defaultValue);
+  public Config getConfig() {
+    return cfg;
   }
 
-  private String buildUrl(String protocol, String hostname, String port) {
-    try {
-      return new URL(protocol, hostname, Integer.parseInt(port), "").toString();
-    } catch (MalformedURLException | NumberFormatException e) {
-      throw new RuntimeException(
-          "Cannot build url to Elasticsearch from values: protocol="
-              + protocol
-              + " hostname="
-              + hostname
-              + " port="
-              + port,
-          e);
-    }
+  public String getIndexName(String name, int schemaVersion) {
+    return String.format("%s%s_%04d", prefix, name, schemaVersion);
+  }
+
+  private String getString(Config cfg, String subsection, String name, String defaultValue) {
+    return MoreObjects.firstNonNull(cfg.getString("elasticsearch", subsection, name), defaultValue);
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticException.java b/java/com/google/gerrit/elasticsearch/ElasticException.java
new file mode 100644
index 0000000..d4baf75
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticException.java
@@ -0,0 +1,27 @@
+// 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.elasticsearch;
+
+class ElasticException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  ElasticException(String message) {
+    super(message);
+  }
+
+  ElasticException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index fa46b3e..cf1a4ed 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.elasticsearch.bulk.BulkRequest;
+import com.google.gerrit.elasticsearch.bulk.IndexRequest;
+import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -23,32 +25,29 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.group.GroupField;
 import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import io.searchbox.client.JestResult;
-import io.searchbox.core.Bulk;
-import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.search.sort.Sort;
 import java.io.IOException;
 import java.util.Set;
-import org.eclipse.jgit.lib.Config;
+import org.apache.http.HttpStatus;
+import org.elasticsearch.client.Response;
 
 public class ElasticGroupIndex extends AbstractElasticIndex<AccountGroup.UUID, InternalGroup>
     implements GroupIndex {
   static class GroupMapping {
     MappingProperties groups;
 
-    GroupMapping(Schema<InternalGroup> schema) {
-      this.groups = ElasticMapping.createMapping(schema);
+    GroupMapping(Schema<InternalGroup> schema, ElasticQueryAdapter adapter) {
+      this.groups = ElasticMapping.createMapping(schema, adapter);
     }
   }
 
@@ -56,54 +55,53 @@
 
   private final GroupMapping mapping;
   private final Provider<GroupCache> groupCache;
+  private final Schema<InternalGroup> schema;
 
   @Inject
   ElasticGroupIndex(
-      @GerritServerConfig Config cfg,
+      ElasticConfiguration cfg,
       SitePaths sitePaths,
       Provider<GroupCache> groupCache,
-      JestClientBuilder clientBuilder,
+      ElasticRestClientProvider client,
       @Assisted Schema<InternalGroup> schema) {
-    super(cfg, sitePaths, schema, clientBuilder, GROUPS);
+    super(cfg, sitePaths, schema, client, GROUPS);
     this.groupCache = groupCache;
-    this.mapping = new GroupMapping(schema);
+    this.mapping = new GroupMapping(schema, client.adapter());
+    this.schema = schema;
   }
 
   @Override
   public void replace(InternalGroup group) throws IOException {
-    Bulk bulk =
-        new Bulk.Builder()
-            .defaultIndex(indexName)
-            .defaultType(GROUPS)
-            .addAction(insert(GROUPS, group))
-            .refresh(true)
-            .build();
-    JestResult result = client.execute(bulk);
-    if (!result.isSucceeded()) {
+    BulkRequest bulk =
+        new IndexRequest(getId(group), indexName, type, client.adapter())
+            .add(new UpdateRequest<>(schema, group));
+
+    String uri = getURI(type, BULK);
+    Response response = postRequest(bulk, uri, getRefreshParam());
+    int statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode != HttpStatus.SC_OK) {
       throw new IOException(
           String.format(
               "Failed to replace group %s in index %s: %s",
-              group.getGroupUUID().get(), indexName, result.getErrorMessage()));
+              group.getGroupUUID().get(), indexName, statusCode));
     }
   }
 
   @Override
   public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
       throws QueryParseException {
-    Sort sort = new Sort(GroupField.UUID.getName(), Sort.Sorting.ASC);
-    sort.setIgnoreUnmapped();
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), GROUPS, sort);
+    JsonArray sortArray = getSortArray(GroupField.UUID.getName());
+    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::groupFields), GROUPS, sortArray);
   }
 
   @Override
-  protected Builder addActions(Builder builder, AccountGroup.UUID c) {
-    return builder.addAction(delete(GROUPS, c));
+  protected String getDeleteActions(AccountGroup.UUID g) {
+    return delete(type, g);
   }
 
   @Override
   protected String getMappings() {
-    ImmutableMap<String, GroupMapping> mappings = ImmutableMap.of("mappings", mapping);
-    return gson.toJson(mappings);
+    return getMappingsForSingleType(GROUPS, mapping.groups);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 76fdfea..1e41985 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -42,6 +42,12 @@
   }
 
   @Override
+  public void configure() {
+    super.configure();
+    install(ElasticRestClientProvider.module());
+  }
+
+  @Override
   protected Class<? extends AccountIndex> getAccountIndex() {
     return ElasticAccountIndex.class;
   }
@@ -63,6 +69,6 @@
 
   @Override
   protected Class<? extends VersionManager> getVersionManager() {
-    return ElasticVersionManager.class;
+    return ElasticIndexVersionManager.class;
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
index b73b37f..3314a38 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
@@ -14,38 +14,39 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gson.JsonParser;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import io.searchbox.client.JestResult;
-import io.searchbox.client.http.JestHttpClient;
-import io.searchbox.indices.aliases.GetAliases;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map.Entry;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.elasticsearch.client.Response;
 
 @Singleton
 class ElasticIndexVersionDiscovery {
-  private final JestHttpClient client;
+  private final ElasticRestClientProvider client;
 
   @Inject
-  ElasticIndexVersionDiscovery(JestClientBuilder clientBuilder) {
-    this.client = clientBuilder.build();
+  ElasticIndexVersionDiscovery(ElasticRestClientProvider client) {
+    this.client = client;
   }
 
   List<String> discover(String prefix, String indexName) throws IOException {
     String name = prefix + indexName + "_";
-    JestResult result = client.execute(new GetAliases.Builder().addIndex(name + "*").build());
-    if (result.isSucceeded()) {
-      JsonObject object = result.getJsonObject().getAsJsonObject();
-      List<String> versions = new ArrayList<>(object.size());
-      for (Entry<String, JsonElement> entry : object.entrySet()) {
-        versions.add(entry.getKey().replace(name, ""));
-      }
-      return versions;
+    Response response = client.get().performRequest(HttpGet.METHOD_NAME, name + "*/_aliases");
+
+    if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+      return new JsonParser()
+          .parse(AbstractElasticIndex.getContent(response))
+          .getAsJsonObject()
+          .entrySet()
+          .stream()
+          .map(e -> e.getKey().replace(name, ""))
+          .collect(toList());
     }
     return Collections.emptyList();
   }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
similarity index 83%
rename from java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
rename to java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
index dce8fac..58272f7 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
@@ -31,18 +32,16 @@
 import java.util.Collection;
 import java.util.TreeMap;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
-public class ElasticVersionManager extends VersionManager {
-  private static final Logger log = LoggerFactory.getLogger(ElasticVersionManager.class);
+public class ElasticIndexVersionManager extends VersionManager {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final String prefix;
   private final ElasticIndexVersionDiscovery versionDiscovery;
 
   @Inject
-  ElasticVersionManager(
+  ElasticIndexVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
       DynamicSet<OnlineUpgradeListener> listeners,
@@ -50,7 +49,7 @@
       ElasticIndexVersionDiscovery versionDiscovery) {
     super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
     this.versionDiscovery = versionDiscovery;
-    prefix = MoreObjects.firstNonNull(cfg.getString("index", null, "prefix"), "gerrit");
+    prefix = Strings.nullToEmpty(cfg.getString("elasticsearch", null, "prefix"));
   }
 
   @Override
@@ -61,13 +60,13 @@
       for (String version : versionDiscovery.discover(prefix, def.getName())) {
         Integer v = Ints.tryParse(version);
         if (v == null || version.length() != 4) {
-          log.warn("Unrecognized version in index {}: {}", def.getName(), version);
+          logger.atWarning().log("Unrecognized version in index %s: %s", def.getName(), version);
           continue;
         }
         versions.put(v, new Version<V>(null, v, true, cfg.getReady(def.getName(), v)));
       }
     } catch (IOException e) {
-      log.error("Error scanning index: " + def.getName(), e);
+      logger.atSevere().withCause(e).log("Error scanning index: %s", def.getName());
     }
 
     for (Schema<V> schema : def.getSchemas().values()) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
index 9e1c729..a30e546 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticMapping.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -21,8 +21,8 @@
 import java.util.Map;
 
 class ElasticMapping {
-  static MappingProperties createMapping(Schema<?> schema) {
-    ElasticMapping.Builder mapping = new ElasticMapping.Builder();
+  static MappingProperties createMapping(Schema<?> schema, ElasticQueryAdapter adapter) {
+    ElasticMapping.Builder mapping = new ElasticMapping.Builder(adapter);
     for (FieldDef<?, ?> field : schema.getFields().values()) {
       String name = field.getName();
       FieldType<?> fieldType = field.getType();
@@ -46,9 +46,14 @@
   }
 
   static class Builder {
+    private final ElasticQueryAdapter adapter;
     private final ImmutableMap.Builder<String, FieldProperties> fields =
         new ImmutableMap.Builder<>();
 
+    Builder(ElasticQueryAdapter adapter) {
+      this.adapter = adapter;
+    }
+
     MappingProperties build() {
       MappingProperties properties = new MappingProperties();
       properties.properties = fields.build();
@@ -56,9 +61,10 @@
     }
 
     Builder addExactField(String name) {
-      FieldProperties key = new FieldProperties("string");
-      key.index = "not_analyzed";
-      FieldProperties properties = new FieldProperties("string");
+      FieldProperties key = new FieldProperties(adapter.exactFieldType());
+      key.index = adapter.indexProperty();
+      FieldProperties properties;
+      properties = new FieldProperties(adapter.exactFieldType());
       properties.fields = ImmutableMap.of("key", key);
       fields.put(name, properties);
       return this;
@@ -78,7 +84,7 @@
     }
 
     Builder addString(String name) {
-      fields.put(name, new FieldProperties("string"));
+      fields.put(name, new FieldProperties(adapter.stringFieldType()));
       return this;
     }
 
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index e9194c7..623f62c 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.elasticsearch.bulk.BulkRequest;
+import com.google.gerrit.elasticsearch.bulk.IndexRequest;
+import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectData;
@@ -25,31 +27,27 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import io.searchbox.client.JestResult;
-import io.searchbox.core.Bulk;
-import io.searchbox.core.Bulk.Builder;
-import io.searchbox.core.search.sort.Sort;
-import io.searchbox.core.search.sort.Sort.Sorting;
 import java.io.IOException;
 import java.util.Set;
-import org.eclipse.jgit.lib.Config;
+import org.apache.http.HttpStatus;
+import org.elasticsearch.client.Response;
 
 public class ElasticProjectIndex extends AbstractElasticIndex<Project.NameKey, ProjectData>
     implements ProjectIndex {
   static class ProjectMapping {
     MappingProperties projects;
 
-    ProjectMapping(Schema<ProjectData> schema) {
-      this.projects = ElasticMapping.createMapping(schema);
+    ProjectMapping(Schema<ProjectData> schema, ElasticQueryAdapter adapter) {
+      this.projects = ElasticMapping.createMapping(schema, adapter);
     }
   }
 
@@ -57,54 +55,53 @@
 
   private final ProjectMapping mapping;
   private final Provider<ProjectCache> projectCache;
+  private final Schema<ProjectData> schema;
 
   @Inject
   ElasticProjectIndex(
-      @GerritServerConfig Config cfg,
+      ElasticConfiguration cfg,
       SitePaths sitePaths,
       Provider<ProjectCache> projectCache,
-      JestClientBuilder clientBuilder,
+      ElasticRestClientProvider client,
       @Assisted Schema<ProjectData> schema) {
-    super(cfg, sitePaths, schema, clientBuilder, PROJECTS);
+    super(cfg, sitePaths, schema, client, PROJECTS);
     this.projectCache = projectCache;
-    this.mapping = new ProjectMapping(schema);
+    this.schema = schema;
+    this.mapping = new ProjectMapping(schema, client.adapter());
   }
 
   @Override
   public void replace(ProjectData projectState) throws IOException {
-    Bulk bulk =
-        new Bulk.Builder()
-            .defaultIndex(indexName)
-            .defaultType(PROJECTS)
-            .addAction(insert(PROJECTS, projectState))
-            .refresh(true)
-            .build();
-    JestResult result = client.execute(bulk);
-    if (!result.isSucceeded()) {
+    BulkRequest bulk =
+        new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter())
+            .add(new UpdateRequest<>(schema, projectState));
+
+    String uri = getURI(type, BULK);
+    Response response = postRequest(bulk, uri, getRefreshParam());
+    int statusCode = response.getStatusLine().getStatusCode();
+    if (statusCode != HttpStatus.SC_OK) {
       throw new IOException(
           String.format(
               "Failed to replace project %s in index %s: %s",
-              projectState.getProject().getName(), indexName, result.getErrorMessage()));
+              projectState.getProject().getName(), indexName, statusCode));
     }
   }
 
   @Override
   public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts)
       throws QueryParseException {
-    Sort sort = new Sort(ProjectField.NAME.getName(), Sorting.ASC);
-    sort.setIgnoreUnmapped();
-    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::projectFields), PROJECTS, sort);
+    JsonArray sortArray = getSortArray(ProjectField.NAME.getName());
+    return new ElasticQuerySource(p, opts.filterFields(IndexUtils::projectFields), type, sortArray);
   }
 
   @Override
-  protected Builder addActions(Builder builder, Project.NameKey nameKey) {
-    return builder.addAction(delete(PROJECTS, nameKey));
+  protected String getDeleteActions(Project.NameKey nameKey) {
+    return delete(type, nameKey);
   }
 
   @Override
   protected String getMappings() {
-    ImmutableMap<String, ProjectMapping> mappings = ImmutableMap.of("mappings", mapping);
-    return gson.toJson(mappings);
+    return getMappingsForSingleType(PROJECTS, mapping.projects);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
new file mode 100644
index 0000000..3201289
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -0,0 +1,101 @@
+// 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.elasticsearch;
+
+import com.google.gson.JsonObject;
+
+public class ElasticQueryAdapter {
+  static final String POST_V5_TYPE = "_doc";
+
+  private final boolean ignoreUnmapped;
+  private final boolean usePostV5Type;
+
+  private final String searchFilteringName;
+  private final String indicesExistParam;
+  private final String exactFieldType;
+  private final String stringFieldType;
+  private final String indexProperty;
+  private final String rawFieldsKey;
+
+  ElasticQueryAdapter(ElasticVersion version) {
+    this.ignoreUnmapped = version == ElasticVersion.V2_4;
+    this.usePostV5Type = version == ElasticVersion.V6_2;
+
+    switch (version) {
+      case V5_6:
+      case V6_2:
+        this.searchFilteringName = "_source";
+        this.indicesExistParam = "?allow_no_indices=false";
+        this.exactFieldType = "keyword";
+        this.stringFieldType = "text";
+        this.indexProperty = "true";
+        this.rawFieldsKey = "_source";
+        break;
+      case V2_4:
+      default:
+        this.searchFilteringName = "fields";
+        this.indicesExistParam = "";
+        this.exactFieldType = "string";
+        this.stringFieldType = "string";
+        this.indexProperty = "not_analyzed";
+        this.rawFieldsKey = "fields";
+        break;
+    }
+  }
+
+  void setIgnoreUnmapped(JsonObject properties) {
+    if (ignoreUnmapped) {
+      properties.addProperty("ignore_unmapped", true);
+    }
+  }
+
+  public void setType(JsonObject properties, String type) {
+    if (!usePostV5Type) {
+      properties.addProperty("_type", type);
+    }
+  }
+
+  public String searchFilteringName() {
+    return searchFilteringName;
+  }
+
+  String indicesExistParam() {
+    return indicesExistParam;
+  }
+
+  String exactFieldType() {
+    return exactFieldType;
+  }
+
+  String stringFieldType() {
+    return stringFieldType;
+  }
+
+  String indexProperty() {
+    return indexProperty;
+  }
+
+  String rawFieldsKey() {
+    return rawFieldsKey;
+  }
+
+  boolean usePostV5Type() {
+    return usePostV5Type;
+  }
+
+  String getType(String preV6Type) {
+    return usePostV5Type() ? POST_V5_TYPE : preV6Type;
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index 6905cf4..2a97e2e 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.elasticsearch;
 
+import com.google.gerrit.elasticsearch.builders.BoolQueryBuilder;
+import com.google.gerrit.elasticsearch.builders.QueryBuilder;
+import com.google.gerrit.elasticsearch.builders.QueryBuilders;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.query.AndPredicate;
@@ -28,10 +31,6 @@
 import com.google.gerrit.index.query.TimestampRangePredicate;
 import com.google.gerrit.server.query.change.AfterPredicate;
 import java.time.Instant;
-import org.apache.lucene.search.BooleanQuery;
-import org.elasticsearch.index.query.BoolQueryBuilder;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.index.query.QueryBuilders;
 
 public class ElasticQueryBuilder {
 
@@ -52,27 +51,19 @@
   }
 
   private <T> BoolQueryBuilder and(Predicate<T> p) throws QueryParseException {
-    try {
-      BoolQueryBuilder b = QueryBuilders.boolQuery();
-      for (Predicate<T> c : p.getChildren()) {
-        b.must(toQueryBuilder(c));
-      }
-      return b;
-    } catch (BooleanQuery.TooManyClauses e) {
-      throw new QueryParseException("cannot create query for index: " + p, e);
+    BoolQueryBuilder b = QueryBuilders.boolQuery();
+    for (Predicate<T> c : p.getChildren()) {
+      b.must(toQueryBuilder(c));
     }
+    return b;
   }
 
   private <T> BoolQueryBuilder or(Predicate<T> p) throws QueryParseException {
-    try {
-      BoolQueryBuilder q = QueryBuilders.boolQuery();
-      for (Predicate<T> c : p.getChildren()) {
-        q.should(toQueryBuilder(c));
-      }
-      return q;
-    } catch (BooleanQuery.TooManyClauses e) {
-      throw new QueryParseException("cannot create query for index: " + p, e);
+    BoolQueryBuilder q = QueryBuilders.boolQuery();
+    for (Predicate<T> c : p.getChildren()) {
+      q.should(toQueryBuilder(c));
     }
+    return q;
   }
 
   private <T> QueryBuilder not(Predicate<T> p) throws QueryParseException {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
new file mode 100644
index 0000000..c2c4548
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
@@ -0,0 +1,149 @@
+// 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.elasticsearch;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestClientBuilder;
+
+@Singleton
+class ElasticRestClientProvider implements Provider<RestClient>, LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final HttpHost[] hosts;
+  private final String username;
+  private final String password;
+
+  private RestClient client;
+  private ElasticQueryAdapter adapter;
+
+  @Inject
+  ElasticRestClientProvider(ElasticConfiguration cfg) {
+    hosts = cfg.urls.toArray(new HttpHost[cfg.urls.size()]);
+    username = cfg.username;
+    password = cfg.password;
+  }
+
+  public static LifecycleModule module() {
+    return new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(ElasticRestClientProvider.class);
+      }
+    };
+  }
+
+  @Override
+  public RestClient get() {
+    if (client == null) {
+      synchronized (this) {
+        if (client == null) {
+          client = build();
+          ElasticVersion version = getVersion();
+          logger.atInfo().log("Elasticsearch integration version %s", version);
+          adapter = new ElasticQueryAdapter(version);
+        }
+      }
+    }
+    return client;
+  }
+
+  @Override
+  public void start() {}
+
+  @Override
+  public void stop() {
+    if (client != null) {
+      try {
+        client.close();
+      } catch (IOException e) {
+        // Ignore. We can't do anything about it.
+      }
+    }
+  }
+
+  ElasticQueryAdapter adapter() {
+    get(); // Make sure we're connected
+    return adapter;
+  }
+
+  public static class FailedToGetVersion extends ElasticException {
+    private static final long serialVersionUID = 1L;
+    private static final String MESSAGE = "Failed to get Elasticsearch version";
+
+    FailedToGetVersion(StatusLine status) {
+      super(String.format("%s: %d %s", MESSAGE, status.getStatusCode(), status.getReasonPhrase()));
+    }
+
+    FailedToGetVersion(Throwable cause) {
+      super(MESSAGE, cause);
+    }
+  }
+
+  private ElasticVersion getVersion() throws ElasticException {
+    try {
+      Response response = client.performRequest("GET", "");
+      StatusLine statusLine = response.getStatusLine();
+      if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
+        throw new FailedToGetVersion(statusLine);
+      }
+      String version =
+          new JsonParser()
+              .parse(AbstractElasticIndex.getContent(response))
+              .getAsJsonObject()
+              .get("version")
+              .getAsJsonObject()
+              .get("number")
+              .getAsString();
+      logger.atInfo().log("Connected to Elasticsearch version %s", version);
+      return ElasticVersion.forVersion(version);
+    } catch (IOException e) {
+      throw new FailedToGetVersion(e);
+    }
+  }
+
+  private RestClient build() {
+    RestClientBuilder builder = RestClient.builder(hosts);
+    setConfiguredCredentialsIfAny(builder);
+    return builder.build();
+  }
+
+  private void setConfiguredCredentialsIfAny(RestClientBuilder builder) {
+    if (username != null && password != null) {
+      CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+      credentialsProvider.setCredentials(
+          AuthScope.ANY, new UsernamePasswordCredentials(username, password));
+      builder.setHttpClientConfigCallback(
+          (HttpAsyncClientBuilder httpClientBuilder) ->
+              httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
new file mode 100644
index 0000000..ff26382
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -0,0 +1,60 @@
+// 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.elasticsearch;
+
+import com.google.common.base.Joiner;
+import java.util.regex.Pattern;
+
+public enum ElasticVersion {
+  V2_4("2.4.*"),
+  V5_6("5.6.*"),
+  V6_2("6.2.*");
+
+  private final String version;
+  private final Pattern pattern;
+
+  private ElasticVersion(String version) {
+    this.version = version;
+    this.pattern = Pattern.compile(version);
+  }
+
+  public static class InvalidVersion extends ElasticException {
+    private static final long serialVersionUID = 1L;
+
+    InvalidVersion(String version) {
+      super(
+          String.format(
+              "Invalid version: [%s]. Supported versions: %s", version, supportedVersions()));
+    }
+  }
+
+  public static ElasticVersion forVersion(String version) throws InvalidVersion {
+    for (ElasticVersion value : ElasticVersion.values()) {
+      if (value.pattern.matcher(version).matches()) {
+        return value;
+      }
+    }
+    throw new InvalidVersion(version);
+  }
+
+  public static String supportedVersions() {
+    return Joiner.on(", ").join(ElasticVersion.values());
+  }
+
+  @Override
+  public String toString() {
+    return version;
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/JestClientBuilder.java b/java/com/google/gerrit/elasticsearch/JestClientBuilder.java
deleted file mode 100644
index c548cb9..0000000
--- a/java/com/google/gerrit/elasticsearch/JestClientBuilder.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2017 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.elasticsearch;
-
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import io.searchbox.client.JestClientFactory;
-import io.searchbox.client.config.HttpClientConfig;
-import io.searchbox.client.config.HttpClientConfig.Builder;
-import io.searchbox.client.http.JestHttpClient;
-import java.util.concurrent.TimeUnit;
-
-@Singleton
-class JestClientBuilder {
-  private final ElasticConfiguration cfg;
-
-  @Inject
-  JestClientBuilder(ElasticConfiguration cfg) {
-    this.cfg = cfg;
-  }
-
-  JestHttpClient build() {
-    JestClientFactory factory = new JestClientFactory();
-    Builder builder =
-        new HttpClientConfig.Builder(cfg.urls)
-            .multiThreaded(true)
-            .discoveryEnabled(false)
-            .connTimeout((int) cfg.connectionTimeout)
-            .maxConnectionIdleTime(cfg.maxConnectionIdleTime, cfg.maxConnectionIdleUnit)
-            .maxTotalConnection(cfg.maxTotalConnection)
-            .readTimeout(cfg.readTimeout)
-            .requestCompressionEnabled(cfg.requestCompression)
-            .discoveryFrequency(1L, TimeUnit.MINUTES);
-
-    if (cfg.username != null && cfg.password != null) {
-      builder.defaultCredentials(cfg.username, cfg.password);
-    }
-
-    factory.setHttpClientConfig(builder.build());
-    return (JestHttpClient) factory.getObject();
-  }
-}
diff --git a/java/com/google/gerrit/elasticsearch/builders/BoolQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/BoolQueryBuilder.java
new file mode 100644
index 0000000..a204919
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/BoolQueryBuilder.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A Query that matches documents matching boolean combinations of other queries.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.BoolQueryBuilder.
+ */
+public class BoolQueryBuilder extends QueryBuilder {
+
+  private final List<QueryBuilder> mustClauses = new ArrayList<>();
+
+  private final List<QueryBuilder> mustNotClauses = new ArrayList<>();
+
+  private final List<QueryBuilder> filterClauses = new ArrayList<>();
+
+  private final List<QueryBuilder> shouldClauses = new ArrayList<>();
+
+  /**
+   * Adds a query that <b>must</b> appear in the matching documents and will contribute to scoring.
+   */
+  public BoolQueryBuilder must(QueryBuilder queryBuilder) {
+    mustClauses.add(queryBuilder);
+    return this;
+  }
+
+  /**
+   * Adds a query that <b>must not</b> appear in the matching documents and will not contribute to
+   * scoring.
+   */
+  public BoolQueryBuilder mustNot(QueryBuilder queryBuilder) {
+    mustNotClauses.add(queryBuilder);
+    return this;
+  }
+
+  /**
+   * Adds a query that <i>should</i> appear in the matching documents. For a boolean query with no
+   * <tt>MUST</tt> clauses one or more <code>SHOULD</code> clauses must match a document for the
+   * BooleanQuery to match.
+   */
+  public BoolQueryBuilder should(QueryBuilder queryBuilder) {
+    shouldClauses.add(queryBuilder);
+    return this;
+  }
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject("bool");
+    doXArrayContent("must", mustClauses, builder);
+    doXArrayContent("filter", filterClauses, builder);
+    doXArrayContent("must_not", mustNotClauses, builder);
+    doXArrayContent("should", shouldClauses, builder);
+    builder.endObject();
+  }
+
+  private void doXArrayContent(String field, List<QueryBuilder> clauses, XContentBuilder builder)
+      throws IOException {
+    if (clauses.isEmpty()) {
+      return;
+    }
+    if (clauses.size() == 1) {
+      builder.field(field);
+      clauses.get(0).toXContent(builder);
+    } else {
+      builder.startArray(field);
+      for (QueryBuilder clause : clauses) {
+        clause.toXContent(builder);
+      }
+      builder.endArray();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/ExistsQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/ExistsQueryBuilder.java
new file mode 100644
index 0000000..1b058d7
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/ExistsQueryBuilder.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/**
+ * Constructs a query that only match on documents that the field has a value in them.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.ExistsQueryBuilder.
+ */
+class ExistsQueryBuilder extends QueryBuilder {
+
+  private final String name;
+
+  ExistsQueryBuilder(String name) {
+    this.name = name;
+  }
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject("exists");
+    builder.field("field", name);
+    builder.endObject();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/MatchAllQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/MatchAllQueryBuilder.java
new file mode 100644
index 0000000..a3b303c
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/MatchAllQueryBuilder.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/**
+ * A query that matches on all documents.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.MatchAllQueryBuilder.
+ */
+class MatchAllQueryBuilder extends QueryBuilder {
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject("match_all");
+    builder.endObject();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/MatchQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/MatchQueryBuilder.java
new file mode 100644
index 0000000..c0becd1
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/MatchQueryBuilder.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * Match query is a query that analyzes the text and constructs a query as the result of the
+ * analysis. It can construct different queries based on the type provided.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.MatchQueryBuilder.
+ */
+class MatchQueryBuilder extends QueryBuilder {
+
+  enum Type {
+    /** The text is analyzed and used as a phrase query. */
+    MATCH_PHRASE,
+    /** The text is analyzed and used in a phrase query, with the last term acting as a prefix. */
+    MATCH_PHRASE_PREFIX;
+
+    @Override
+    public String toString() {
+      return name().toLowerCase(Locale.US);
+    }
+  }
+
+  private final String name;
+
+  private final Object text;
+
+  private Type type;
+
+  /** Constructs a new text query. */
+  MatchQueryBuilder(String name, Object text) {
+    this.name = name;
+    this.text = text;
+  }
+
+  /** Sets the type of the text query. */
+  MatchQueryBuilder type(Type type) {
+    this.type = type;
+    return this;
+  }
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject(type.toString()).field(name, text).endObject();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/QueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/QueryBuilder.java
new file mode 100644
index 0000000..d6f154e
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/QueryBuilder.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/** A trimmed down version of org.elasticsearch.index.query.QueryBuilder. */
+public abstract class QueryBuilder {
+
+  protected QueryBuilder() {}
+
+  protected void toXContent(XContentBuilder builder) throws IOException {
+    builder.startObject();
+    doXContent(builder);
+    builder.endObject();
+  }
+
+  protected abstract void doXContent(XContentBuilder builder) throws IOException;
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/QueryBuilders.java b/java/com/google/gerrit/elasticsearch/builders/QueryBuilders.java
new file mode 100644
index 0000000..940146f
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/QueryBuilders.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+/**
+ * A static factory for simple "import static" usage.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.QueryBuilders.
+ */
+public abstract class QueryBuilders {
+
+  /** A query that match on all documents. */
+  public static MatchAllQueryBuilder matchAllQuery() {
+    return new MatchAllQueryBuilder();
+  }
+
+  /**
+   * Creates a text query with type "PHRASE" for the provided field name and text.
+   *
+   * @param name The field name.
+   * @param text The query text (to be analyzed).
+   */
+  public static MatchQueryBuilder matchPhraseQuery(String name, Object text) {
+    return new MatchQueryBuilder(name, text).type(MatchQueryBuilder.Type.MATCH_PHRASE);
+  }
+
+  /**
+   * Creates a match query with type "PHRASE_PREFIX" for the provided field name and text.
+   *
+   * @param name The field name.
+   * @param text The query text (to be analyzed).
+   */
+  public static MatchQueryBuilder matchPhrasePrefixQuery(String name, Object text) {
+    return new MatchQueryBuilder(name, text).type(MatchQueryBuilder.Type.MATCH_PHRASE_PREFIX);
+  }
+
+  /**
+   * A Query that matches documents containing a term.
+   *
+   * @param name The name of the field
+   * @param value The value of the term
+   */
+  public static TermQueryBuilder termQuery(String name, String value) {
+    return new TermQueryBuilder(name, value);
+  }
+
+  /**
+   * A Query that matches documents containing a term.
+   *
+   * @param name The name of the field
+   * @param value The value of the term
+   */
+  public static TermQueryBuilder termQuery(String name, int value) {
+    return new TermQueryBuilder(name, value);
+  }
+
+  /**
+   * A Query that matches documents within an range of terms.
+   *
+   * @param name The field name
+   */
+  public static RangeQueryBuilder rangeQuery(String name) {
+    return new RangeQueryBuilder(name);
+  }
+
+  /**
+   * A Query that matches documents containing terms with a specified regular expression.
+   *
+   * @param name The name of the field
+   * @param regexp The regular expression
+   */
+  public static RegexpQueryBuilder regexpQuery(String name, String regexp) {
+    return new RegexpQueryBuilder(name, regexp);
+  }
+
+  /** A Query that matches documents matching boolean combinations of other queries. */
+  public static BoolQueryBuilder boolQuery() {
+    return new BoolQueryBuilder();
+  }
+
+  /**
+   * A filter to filter only documents where a field exists in them.
+   *
+   * @param name The name of the field
+   */
+  public static ExistsQueryBuilder existsQuery(String name) {
+    return new ExistsQueryBuilder(name);
+  }
+
+  private QueryBuilders() {}
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/QuerySourceBuilder.java b/java/com/google/gerrit/elasticsearch/builders/QuerySourceBuilder.java
new file mode 100644
index 0000000..1cb5c82
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/QuerySourceBuilder.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/** A trimmed down and modified version of org.elasticsearch.action.support.QuerySourceBuilder. */
+class QuerySourceBuilder {
+
+  private final QueryBuilder queryBuilder;
+
+  QuerySourceBuilder(QueryBuilder queryBuilder) {
+    this.queryBuilder = queryBuilder;
+  }
+
+  void innerToXContent(XContentBuilder builder) throws IOException {
+    builder.field("query");
+    queryBuilder.toXContent(builder);
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/RangeQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/RangeQueryBuilder.java
new file mode 100644
index 0000000..32dbc0e
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/RangeQueryBuilder.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/**
+ * A Query that matches documents within an range of terms.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.RangeQueryBuilder.
+ */
+public class RangeQueryBuilder extends QueryBuilder {
+
+  private final String name;
+  private Object from;
+  private Object to;
+  private boolean includeLower = true;
+  private boolean includeUpper = true;
+
+  /**
+   * A Query that matches documents within an range of terms.
+   *
+   * @param name The field name
+   */
+  RangeQueryBuilder(String name) {
+    this.name = name;
+  }
+
+  /** The from part of the range query. Null indicates unbounded. */
+  public RangeQueryBuilder gt(Object from) {
+    this.from = from;
+    this.includeLower = false;
+    return this;
+  }
+
+  /** The from part of the range query. Null indicates unbounded. */
+  public RangeQueryBuilder gte(Object from) {
+    this.from = from;
+    this.includeLower = true;
+    return this;
+  }
+
+  /** The from part of the range query. Null indicates unbounded. */
+  public RangeQueryBuilder gte(int from) {
+    this.from = from;
+    this.includeLower = true;
+    return this;
+  }
+
+  /** The to part of the range query. Null indicates unbounded. */
+  public RangeQueryBuilder lte(Object to) {
+    this.to = to;
+    this.includeUpper = true;
+    return this;
+  }
+
+  /** The to part of the range query. Null indicates unbounded. */
+  public RangeQueryBuilder lte(int to) {
+    this.to = to;
+    this.includeUpper = true;
+    return this;
+  }
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject("range");
+    builder.startObject(name);
+
+    builder.field("from", from);
+    builder.field("to", to);
+    builder.field("include_lower", includeLower);
+    builder.field("include_upper", includeUpper);
+
+    builder.endObject();
+    builder.endObject();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/RegexpQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/RegexpQueryBuilder.java
new file mode 100644
index 0000000..b81ec20
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/RegexpQueryBuilder.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/**
+ * A Query that does fuzzy matching for a specific value.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.RegexpQueryBuilder.
+ */
+class RegexpQueryBuilder extends QueryBuilder {
+
+  private final String name;
+  private final String regexp;
+
+  /**
+   * Constructs a new term query.
+   *
+   * @param name The name of the field
+   * @param regexp The regular expression
+   */
+  RegexpQueryBuilder(String name, String regexp) {
+    this.name = name;
+    this.regexp = regexp;
+  }
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject("regexp");
+    builder.startObject(name);
+
+    builder.field("value", regexp);
+    builder.field("flags_value", 65535);
+
+    builder.endObject();
+    builder.endObject();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/SearchSourceBuilder.java b/java/com/google/gerrit/elasticsearch/builders/SearchSourceBuilder.java
new file mode 100644
index 0000000..35cbea9
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/SearchSourceBuilder.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A search source builder allowing to easily build search source.
+ *
+ * <p>A trimmed down and modified version of org.elasticsearch.search.builder.SearchSourceBuilder.
+ */
+public class SearchSourceBuilder {
+  private final ElasticQueryAdapter adapter;
+
+  private QuerySourceBuilder querySourceBuilder;
+
+  private int from = -1;
+
+  private int size = -1;
+
+  private List<String> fieldNames;
+
+  /** Constructs a new search source builder. */
+  public SearchSourceBuilder(ElasticQueryAdapter adapter) {
+    this.adapter = adapter;
+  }
+
+  /** Constructs a new search source builder with a search query. */
+  public SearchSourceBuilder query(QueryBuilder query) {
+    if (this.querySourceBuilder == null) {
+      this.querySourceBuilder = new QuerySourceBuilder(query);
+    }
+    return this;
+  }
+
+  /** From index to start the search from. Defaults to <tt>0</tt>. */
+  public SearchSourceBuilder from(int from) {
+    this.from = from;
+    return this;
+  }
+
+  /** The number of search hits to return. Defaults to <tt>10</tt>. */
+  public SearchSourceBuilder size(int size) {
+    this.size = size;
+    return this;
+  }
+
+  /**
+   * Sets the fields to load and return as part of the search request. If none are specified, the
+   * source of the document will be returned.
+   */
+  public SearchSourceBuilder fields(List<String> fields) {
+    this.fieldNames = fields;
+    return this;
+  }
+
+  @Override
+  public final String toString() {
+    try {
+      XContentBuilder builder = new XContentBuilder();
+      toXContent(builder);
+      return builder.string();
+    } catch (IOException ioe) {
+      return "";
+    }
+  }
+
+  private void toXContent(XContentBuilder builder) throws IOException {
+    builder.startObject();
+    innerToXContent(builder);
+    builder.endObject();
+  }
+
+  private void innerToXContent(XContentBuilder builder) throws IOException {
+    if (from != -1) {
+      builder.field("from", from);
+    }
+    if (size != -1) {
+      builder.field("size", size);
+    }
+
+    if (querySourceBuilder != null) {
+      querySourceBuilder.innerToXContent(builder);
+    }
+
+    if (fieldNames != null) {
+      if (fieldNames.size() == 1) {
+        builder.field(adapter.searchFilteringName(), fieldNames.get(0));
+      } else {
+        builder.startArray(adapter.searchFilteringName());
+        for (String fieldName : fieldNames) {
+          builder.value(fieldName);
+        }
+        builder.endArray();
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/TermQueryBuilder.java b/java/com/google/gerrit/elasticsearch/builders/TermQueryBuilder.java
new file mode 100644
index 0000000..2b407c6
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/TermQueryBuilder.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import java.io.IOException;
+
+/**
+ * A Query that matches documents containing a term.
+ *
+ * <p>A trimmed down version of org.elasticsearch.index.query.TermQueryBuilder.
+ */
+class TermQueryBuilder extends QueryBuilder {
+
+  private final String name;
+
+  private final Object value;
+
+  /**
+   * Constructs a new term query.
+   *
+   * @param name The name of the field
+   * @param value The value of the term
+   */
+  TermQueryBuilder(String name, String value) {
+    this(name, (Object) value);
+  }
+
+  /**
+   * Constructs a new term query.
+   *
+   * @param name The name of the field
+   * @param value The value of the term
+   */
+  TermQueryBuilder(String name, int value) {
+    this(name, (Object) value);
+  }
+
+  /**
+   * Constructs a new term query.
+   *
+   * @param name The name of the field
+   * @param value The value of the term
+   */
+  private TermQueryBuilder(String name, Object value) {
+    this.name = name;
+    this.value = value;
+  }
+
+  @Override
+  protected void doXContent(XContentBuilder builder) throws IOException {
+    builder.startObject("term");
+    builder.field(name, value);
+    builder.endObject();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/builders/XContentBuilder.java b/java/com/google/gerrit/elasticsearch/builders/XContentBuilder.java
new file mode 100644
index 0000000..06427f1
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/XContentBuilder.java
@@ -0,0 +1,167 @@
+// Copyright (C) 2018 The Android Open Source Project, 2009-2015 Elasticsearch
+//
+// 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.elasticsearch.builders;
+
+import static java.time.format.DateTimeFormatter.ISO_INSTANT;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.google.common.base.Charsets;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Date;
+
+/** A trimmed down and modified version of org.elasticsearch.common.xcontent.XContentBuilder. */
+public final class XContentBuilder implements Closeable {
+
+  private final JsonGenerator generator;
+
+  private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+  /**
+   * Constructs a new builder. Make sure to call {@link #close()} when the builder is done with.
+   * Inspired from org.elasticsearch.common.xcontent.json.JsonXContent static block.
+   */
+  public XContentBuilder() throws IOException {
+    JsonFactory jsonFactory = new JsonFactory();
+    jsonFactory.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+    jsonFactory.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
+    jsonFactory.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+    jsonFactory.configure(
+        JsonFactory.Feature.FAIL_ON_SYMBOL_HASH_OVERFLOW,
+        false); // this trips on many mappings now...
+    this.generator = jsonFactory.createGenerator(bos, JsonEncoding.UTF8);
+  }
+
+  public XContentBuilder startObject(String name) throws IOException {
+    field(name);
+    startObject();
+    return this;
+  }
+
+  public XContentBuilder startObject() throws IOException {
+    generator.writeStartObject();
+    return this;
+  }
+
+  public XContentBuilder endObject() throws IOException {
+    generator.writeEndObject();
+    return this;
+  }
+
+  public void startArray(String name) throws IOException {
+    field(name);
+    startArray();
+  }
+
+  private void startArray() throws IOException {
+    generator.writeStartArray();
+  }
+
+  public void endArray() throws IOException {
+    generator.writeEndArray();
+  }
+
+  public XContentBuilder field(String name) throws IOException {
+    generator.writeFieldName(name);
+    return this;
+  }
+
+  public XContentBuilder field(String name, String value) throws IOException {
+    field(name);
+    generator.writeString(value);
+    return this;
+  }
+
+  public XContentBuilder field(String name, int value) throws IOException {
+    field(name);
+    generator.writeNumber(value);
+    return this;
+  }
+
+  public XContentBuilder field(String name, Iterable<?> value) throws IOException {
+    startArray(name);
+    for (Object o : value) {
+      value(o);
+    }
+    endArray();
+    return this;
+  }
+
+  public XContentBuilder field(String name, Object value) throws IOException {
+    field(name);
+    writeValue(value);
+    return this;
+  }
+
+  public XContentBuilder value(Object value) throws IOException {
+    writeValue(value);
+    return this;
+  }
+
+  public XContentBuilder field(String name, boolean value) throws IOException {
+    field(name);
+    generator.writeBoolean(value);
+    return this;
+  }
+
+  public XContentBuilder value(String value) throws IOException {
+    generator.writeString(value);
+    return this;
+  }
+
+  @Override
+  public void close() {
+    try {
+      generator.close();
+    } catch (IOException e) {
+      // ignore
+    }
+  }
+
+  /** Returns a string representation of the builder (only applicable for text based xcontent). */
+  public String string() {
+    close();
+    byte[] bytesArray = bos.toByteArray();
+    return new String(bytesArray, Charsets.UTF_8);
+  }
+
+  private void writeValue(Object value) throws IOException {
+    if (value == null) {
+      generator.writeNull();
+      return;
+    }
+    Class<?> type = value.getClass();
+    if (type == String.class) {
+      generator.writeString((String) value);
+    } else if (type == Integer.class) {
+      generator.writeNumber(((Integer) value));
+    } else if (type == byte[].class) {
+      generator.writeBinary((byte[]) value);
+    } else if (value instanceof Date) {
+      generator.writeString(ISO_INSTANT.format(((Date) value).toInstant()));
+    } else {
+      // if this is a "value" object, like enum, DistanceUnit, ..., just toString it
+      // yea, it can be misleading when toString a Java class, but really, jackson should be used in
+      // that case
+      generator.writeString(value.toString());
+      // throw new ElasticsearchIllegalArgumentException("type not supported for generic value
+      // conversion: " + type);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java b/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java
new file mode 100644
index 0000000..7392d09
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java
@@ -0,0 +1,48 @@
+// 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.elasticsearch.bulk;
+
+import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
+import com.google.gson.JsonObject;
+
+abstract class ActionRequest extends BulkRequest {
+
+  private final String action;
+  private final String id;
+  private final String index;
+  private final String type;
+  private final ElasticQueryAdapter adapter;
+
+  protected ActionRequest(
+      String action, String id, String index, String type, ElasticQueryAdapter adapter) {
+    this.action = action;
+    this.id = id;
+    this.index = index;
+    this.type = type;
+    this.adapter = adapter;
+  }
+
+  @Override
+  protected String getRequest() {
+    JsonObject properties = new JsonObject();
+    properties.addProperty("_id", id);
+    properties.addProperty("_index", index);
+    adapter.setType(properties, type);
+
+    JsonObject jsonAction = new JsonObject();
+    jsonAction.add(action, properties);
+    return jsonAction.toString() + System.lineSeparator();
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/bulk/BulkRequest.java b/java/com/google/gerrit/elasticsearch/bulk/BulkRequest.java
new file mode 100644
index 0000000..be5ad8d
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/BulkRequest.java
@@ -0,0 +1,43 @@
+// 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.elasticsearch.bulk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class BulkRequest {
+
+  private final List<BulkRequest> requests = new ArrayList<>();
+
+  protected BulkRequest() {
+    add(this);
+  }
+
+  public BulkRequest add(BulkRequest request) {
+    requests.add(request);
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    for (BulkRequest request : requests) {
+      builder.append(request.getRequest());
+    }
+    return builder.toString();
+  }
+
+  protected abstract String getRequest();
+}
diff --git a/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java b/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java
new file mode 100644
index 0000000..570d5a0
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java
@@ -0,0 +1,24 @@
+// 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.elasticsearch.bulk;
+
+import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
+
+public class DeleteRequest extends ActionRequest {
+
+  public DeleteRequest(String id, String index, String type, ElasticQueryAdapter adapter) {
+    super("delete", id, index, type, adapter);
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java b/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java
new file mode 100644
index 0000000..c571a0e
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java
@@ -0,0 +1,24 @@
+// 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.elasticsearch.bulk;
+
+import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
+
+public class IndexRequest extends ActionRequest {
+
+  public IndexRequest(String id, String index, String type, ElasticQueryAdapter adapter) {
+    super("index", id, index, type, adapter);
+  }
+}
diff --git a/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
new file mode 100644
index 0000000..a693f6d
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/UpdateRequest.java
@@ -0,0 +1,64 @@
+// 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.elasticsearch.bulk;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import com.google.gerrit.elasticsearch.builders.XContentBuilder;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.Schema.Values;
+import java.io.IOException;
+
+public class UpdateRequest<V> extends BulkRequest {
+
+  private final Schema<V> schema;
+  private final V v;
+
+  public UpdateRequest(Schema<V> schema, V v) {
+    this.schema = schema;
+    this.v = v;
+  }
+
+  @Override
+  protected String getRequest() {
+    try (XContentBuilder closeable = new XContentBuilder()) {
+      XContentBuilder builder = closeable.startObject();
+      for (Values<V> values : schema.buildFields(v)) {
+        String name = values.getField().getName();
+        if (values.getField().isRepeatable()) {
+          builder.field(
+              name,
+              Streams.stream(values.getValues())
+                  .filter(e -> shouldAddElement(e))
+                  .collect(toList()));
+        } else {
+          Object element = Iterables.getOnlyElement(values.getValues(), "");
+          if (shouldAddElement(element)) {
+            builder.field(name, element);
+          }
+        }
+      }
+      return builder.endObject().string() + System.lineSeparator();
+    } catch (IOException e) {
+      return e.toString();
+    }
+  }
+
+  private boolean shouldAddElement(Object element) {
+    return !(element instanceof String) || !((String) element).isEmpty();
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/PathSubject.java b/java/com/google/gerrit/extensions/common/testing/PathSubject.java
deleted file mode 100644
index 0b6917c..0000000
--- a/java/com/google/gerrit/extensions/common/testing/PathSubject.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2017 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.extensions.common.testing;
-
-import static com.google.common.truth.Truth.assertAbout;
-
-import com.google.common.truth.FailureMetadata;
-import com.google.common.truth.Subject;
-import java.nio.file.Path;
-
-public class PathSubject extends Subject<PathSubject, Path> {
-  private PathSubject(FailureMetadata failureMetadata, Path path) {
-    super(failureMetadata, path);
-  }
-
-  public static PathSubject assertThat(Path path) {
-    return assertAbout(PathSubject::new).that(path);
-  }
-}
diff --git a/java/com/google/gerrit/extensions/restapi/RestCollection.java b/java/com/google/gerrit/extensions/restapi/RestCollection.java
index 46a4984..e79bde4 100644
--- a/java/com/google/gerrit/extensions/restapi/RestCollection.java
+++ b/java/com/google/gerrit/extensions/restapi/RestCollection.java
@@ -65,10 +65,11 @@
    * "q" parameter option to narrow the results.
    *
    * @return view to list the collection.
-   * @throws ResourceNotFoundException if the collection cannot be listed.
+   * @throws ResourceNotFoundException if the collection doesn't support listing.
    * @throws AuthException if the collection requires authentication.
+   * @throws RestApiException if the collection cannot be listed.
    */
-  RestView<P> list() throws ResourceNotFoundException, AuthException;
+  RestView<P> list() throws RestApiException;
 
   /**
    * Parse a path component into a resource handle.
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index bd6edab..0aa6ca2 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -11,9 +11,9 @@
         "//lib:gwtorm",
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov-neverlink",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index ffedcfb..bc0cd89 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -20,6 +20,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.server.IdentifiedUser;
@@ -44,8 +45,6 @@
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.PushCertificateIdent;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Checker for GPG public keys including Gerrit-specific checks.
@@ -54,7 +53,7 @@
  * ID in the database, or an email address thereof.
  */
 public class GerritPublicKeyChecker extends PublicKeyChecker {
-  private static final Logger log = LoggerFactory.getLogger(GerritPublicKeyChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Singleton
   public static class Factory {
@@ -137,7 +136,7 @@
       return checkIdsForArbitraryUser(key);
     } catch (PGPException | OrmException e) {
       String msg = "Error checking user IDs for key";
-      log.warn(msg + " " + keyIdToString(key.getKeyID()), e);
+      logger.atWarning().withCause(e).log("%s %s", msg, keyIdToString(key.getKeyID()));
       return CheckResult.bad(msg);
     }
   }
diff --git a/java/com/google/gerrit/gpg/GpgModule.java b/java/com/google/gerrit/gpg/GpgModule.java
index d12e921..45c1ab5 100644
--- a/java/com/google/gerrit/gpg/GpgModule.java
+++ b/java/com/google/gerrit/gpg/GpgModule.java
@@ -14,15 +14,14 @@
 
 package com.google.gerrit.gpg;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.gpg.api.GpgApiModule;
 import com.google.gerrit.server.EnableSignedPush;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class GpgModule extends FactoryModule {
-  private static final Logger log = LoggerFactory.getLogger(GpgModule.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Config cfg;
 
@@ -39,7 +38,7 @@
     bindConstant().annotatedWith(EnableSignedPush.class).to(enableSignedPush);
 
     if (configEnableSignedPush && !havePgp) {
-      log.info("Bouncy Castle PGP not installed; signed push verification is disabled");
+      logger.atInfo().log("Bouncy Castle PGP not installed; signed push verification is disabled");
     }
     if (enableSignedPush) {
       install(new SignedPushModule());
diff --git a/java/com/google/gerrit/gpg/PublicKeyChecker.java b/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 70e9a24..07b42f1 100644
--- a/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.gpg;
 
+import static com.google.common.flogger.LazyArgs.lazy;
 import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
 import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK;
 import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
@@ -28,6 +29,7 @@
 import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
 import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -49,12 +51,10 @@
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Checker for GPG public keys for use in a push certificate. */
 public class PublicKeyChecker {
-  private static final Logger log = LoggerFactory.getLogger(PublicKeyChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   // https://tools.ietf.org/html/rfc4880#section-5.2.3.13
   private static final int COMPLETE_TRUST = 120;
@@ -294,12 +294,10 @@
         // Revoker is authorized and there is a revocation signature by this
         // revoker, but the key is not in the store so we can't verify the
         // signature.
-        log.info(
-            "Key "
-                + Fingerprint.toString(key.getFingerprint())
-                + " is revoked by "
-                + Fingerprint.toString(rfp)
-                + ", which is not in the store. Assuming revocation is valid.");
+        logger.atInfo().log(
+            "Key %s is revoked by %s, which is not in the store. Assuming revocation is valid.",
+            lazy(() -> Fingerprint.toString(key.getFingerprint())),
+            lazy(() -> Fingerprint.toString(rfp)));
         problems.add(reasonToString(getRevocationReason(revocation)));
         continue;
       }
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 95b89d0..82b3892 100644
--- a/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 
 import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -38,12 +39,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Checker for push certificates. */
 public abstract class PushCertificateChecker {
-  private static final Logger log = LoggerFactory.getLogger(PushCertificateChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Result {
     private final PGPPublicKey key;
@@ -107,7 +106,7 @@
       }
     } catch (PGPException | IOException e) {
       String msg = "Internal error checking push certificate";
-      log.error(msg, e);
+      logger.atSevere().withCause(e).log(msg);
       results.add(CheckResult.bad(msg));
     }
 
diff --git a/java/com/google/gerrit/gpg/SignedPushModule.java b/java/com/google/gerrit/gpg/SignedPushModule.java
index c420f6f..a051861 100644
--- a/java/com/google/gerrit/gpg/SignedPushModule.java
+++ b/java/com/google/gerrit/gpg/SignedPushModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.gpg;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Project;
@@ -42,11 +43,9 @@
 import org.eclipse.jgit.transport.PreReceiveHookChain;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.SignedPushConfig;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class SignedPushModule extends AbstractModule {
-  private static final Logger log = LoggerFactory.getLogger(SignedPushModule.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   protected void configure() {
@@ -93,8 +92,8 @@
         rp.setSignedPushConfig(null);
         return;
       } else if (signedPushConfig == null) {
-        log.error(
-            "receive.enableSignedPush is true for project {} but"
+        logger.atSevere().log(
+            "receive.enableSignedPush is true for project %s but"
                 + " false in gerrit.config, so signed push verification is"
                 + " disabled",
             project.get());
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index c4e35e0..a2d901f 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -53,12 +54,10 @@
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.eclipse.jgit.util.NB;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
-  private static final Logger log = LoggerFactory.getLogger(GpgKeys.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String MIME_TYPE = "application/pgp-keys";
 
@@ -164,7 +163,8 @@
             }
           }
           if (!found) {
-            log.warn("No public key stored for fingerprint {}", Fingerprint.toString(fp));
+            logger.atWarning().log(
+                "No public key stored for fingerprint %s", Fingerprint.toString(fp));
           }
         }
       }
@@ -207,7 +207,7 @@
     if (!BouncyCastleUtil.havePGP()) {
       throw new ResourceNotFoundException("GPG not enabled");
     }
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       throw new ResourceNotFoundException();
     }
   }
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 4b92ec3..7d08fca 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
@@ -70,12 +71,11 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput> {
-  private final Logger log = LoggerFactory.getLogger(getClass());
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final Provider<PersonIdent> serverIdent;
   private final Provider<CurrentUser> self;
   private final Provider<PublicKeyStore> storeProvider;
@@ -223,10 +223,9 @@
           try {
             addKeyFactory.create(rsrc.getUser(), addedKeys).send();
           } catch (EmailException e) {
-            log.error(
-                "Cannot send GPG key added message to "
-                    + rsrc.getUser().getAccount().getPreferredEmail(),
-                e);
+            logger.atSevere().withCause(e).log(
+                "Cannot send GPG key added message to %s",
+                rsrc.getUser().getAccount().getPreferredEmail());
           }
           break;
         case NO_CHANGE:
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index b62c887..bbb5b66 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -18,8 +18,6 @@
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/http",
-        "//java/com/google/gwtexpui/linker:server",
-        "//java/com/google/gwtexpui/server",
         "//java/org/eclipse/jgit:server",
         "//lib:args4j",
         "//lib:gson",
@@ -32,11 +30,12 @@
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:codec",
+        "//lib/commons:lang",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/java/com/google/gerrit/httpd/DirectChangeByCommit.java
index 26e4198..152a83d 100644
--- a/java/com/google/gerrit/httpd/DirectChangeByCommit.java
+++ b/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -4,6 +4,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -17,13 +18,12 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class DirectChangeByCommit extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private static final Logger log = LoggerFactory.getLogger(DirectChangeByCommit.class);
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Changes changes;
 
@@ -39,7 +39,7 @@
     try {
       results = changes.query(query).withLimit(2).get();
     } catch (RestApiException e) {
-      log.warn("Cannot process query by URL: /r/" + query, e);
+      logger.atWarning().withCause(e).log("Cannot process query by URL: /r/%s", query);
       results = ImmutableList.of();
     }
     String token;
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 739726e..c1a75bb 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -44,10 +44,10 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -119,7 +119,7 @@
             protected void configure() {
               cache(ID_CACHE, AdvertisedObjectsCacheKey.class, new TypeLiteral<Set<ObjectId>>() {})
                   .maximumWeight(4096)
-                  .expireAfterWrite(10, TimeUnit.MINUTES);
+                  .expireAfterWrite(Duration.ofMinutes(10));
             }
           });
     }
diff --git a/java/com/google/gwtexpui/server/CacheControlFilter.java b/java/com/google/gerrit/httpd/GwtCacheControlFilter.java
similarity index 94%
rename from java/com/google/gwtexpui/server/CacheControlFilter.java
rename to java/com/google/gerrit/httpd/GwtCacheControlFilter.java
index 571f72d..5ac3d2f 100644
--- a/java/com/google/gwtexpui/server/CacheControlFilter.java
+++ b/java/com/google/gerrit/httpd/GwtCacheControlFilter.java
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gwtexpui.server;
+package com.google.gerrit.httpd;
 
+import com.google.gerrit.util.http.CacheHeaders;
+import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 import javax.servlet.Filter;
@@ -46,7 +48,8 @@
  *   &lt;/filter-mapping&gt;
  * </pre>
  */
-public class CacheControlFilter implements Filter {
+@Singleton
+class GwtCacheControlFilter implements Filter {
   @Override
   public void init(FilterConfig config) {}
 
diff --git a/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java b/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
index c466290..caced27 100644
--- a/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.httpd;
 
-import static java.util.concurrent.TimeUnit.MINUTES;
-
 import com.google.common.cache.Cache;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -30,6 +28,7 @@
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.name.Named;
 import com.google.inject.servlet.RequestScoped;
+import java.time.Duration;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -41,10 +40,8 @@
       protected void configure() {
         persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
             .maximumWeight(1024) // reasonable default for many sites
-            .expireAfterWrite(
-                CacheBasedWebSession.MAX_AGE_MINUTES,
-                MINUTES) // expire sessions if they are inactive
-        ;
+            // expire sessions if they are inactive
+            .expireAfterWrite(Duration.ofMinutes(CacheBasedWebSession.MAX_AGE_MINUTES));
         install(new FactoryModuleBuilder().build(WebSessionManagerFactory.class));
         DynamicItem.itemOf(binder(), WebSession.class);
         DynamicItem.bind(binder(), WebSession.class)
diff --git a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
index 6774ec80..397d093 100644
--- a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
+++ b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponseWrapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * HttpServletResponse wrapper to allow response status code override.
@@ -29,7 +28,7 @@
  * override the response http status code.
  */
 public class HttpServletResponseRecorder extends HttpServletResponseWrapper {
-  private static final Logger log = LoggerFactory.getLogger(HttpServletResponseRecorder.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final String LOCATION_HEADER = "Location";
 
   private int status;
@@ -78,7 +77,7 @@
 
   void play() throws IOException {
     if (status != 0) {
-      log.debug("Replaying {} {}", status, statusMsg);
+      logger.atFine().log("Replaying %s %s", status, statusMsg);
 
       if (status == SC_MOVED_TEMPORARILY) {
         super.sendRedirect(headers.get(LOCATION_HEADER));
diff --git a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 6174644..818827c 100644
--- a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Account;
@@ -47,8 +48,6 @@
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponseWrapper;
 import org.apache.commons.codec.binary.Base64;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Authenticates the current user by HTTP basic authentication.
@@ -62,7 +61,7 @@
  */
 @Singleton
 class ProjectBasicAuthFilter implements Filter {
-  private static final Logger log = LoggerFactory.getLogger(ProjectBasicAuthFilter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String REALM_NAME = "Gerrit Code Review";
   private static final String AUTHORIZATION = "Authorization";
@@ -131,10 +130,8 @@
     Optional<AccountState> accountState =
         accountCache.getByUsername(username).filter(a -> a.getAccount().isActive());
     if (!accountState.isPresent()) {
-      log.warn(
-          "Authentication failed for "
-              + username
-              + ": account inactive or not provisioned in Gerrit");
+      logger.atWarning().log(
+          "Authentication failed for %s: account inactive or not provisioned in Gerrit", username);
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
@@ -163,17 +160,17 @@
       if (who.checkPassword(password, username)) {
         return succeedAuthentication(who);
       }
-      log.warn(authenticationFailedMsg(username, req), e);
+      logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     } catch (AuthenticationFailedException e) {
       // This exception is thrown if the user provided wrong credentials, we don't need to log a
       // stacktrace for it.
-      log.warn(authenticationFailedMsg(username, req) + ": " + e.getMessage());
+      logger.atWarning().log(authenticationFailedMsg(username, req) + ": %s", e.getMessage());
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     } catch (AccountException e) {
-      log.warn(authenticationFailedMsg(username, req), e);
+      logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
@@ -186,10 +183,9 @@
 
   private boolean failAuthentication(Response rsp, String username, HttpServletRequest req)
       throws IOException {
-    log.warn(
+    logger.atWarning().log(
         authenticationFailedMsg(username, req)
-            + ": password does not match the one stored in Gerrit",
-        username);
+            + ": password does not match the one stored in Gerrit");
     rsp.sendError(SC_UNAUTHORIZED);
     return false;
   }
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index 2b37378..589448e 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -21,6 +21,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -54,8 +55,6 @@
 import javax.servlet.http.HttpServletResponseWrapper;
 import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Authenticates the current user with an OAuth2 server.
@@ -64,8 +63,7 @@
  */
 @Singleton
 class ProjectOAuthFilter implements Filter {
-
-  private static final Logger log = LoggerFactory.getLogger(ProjectOAuthFilter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String REALM_NAME = "Gerrit Code Review";
   private static final String AUTHORIZATION = "Authorization";
@@ -156,7 +154,7 @@
     Optional<AccountState> who =
         accountCache.getByUsername(authInfo.username).filter(a -> a.getAccount().isActive());
     if (!who.isPresent()) {
-      log.warn(
+      logger.atWarning().log(
           authenticationFailedMsg(authInfo.username, req)
               + ": account inactive or not provisioned in Gerrit");
       rsp.sendError(SC_UNAUTHORIZED);
@@ -179,7 +177,7 @@
       ws.setAccessPathOk(AccessPath.REST_API, true);
       return true;
     } catch (AccountException e) {
-      log.warn(authenticationFailedMsg(authInfo.username, req), e);
+      logger.atWarning().withCause(e).log(authenticationFailedMsg(authInfo.username, req));
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
diff --git a/java/com/google/gerrit/httpd/QueryDocumentationFilter.java b/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
index 7a89b3b..8b82c00 100644
--- a/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
+++ b/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor.DocQueryException;
@@ -32,12 +33,10 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class QueryDocumentationFilter implements Filter {
-  private final Logger log = LoggerFactory.getLogger(QueryDocumentationFilter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final QueryDocumentationExecutor searcher;
 
@@ -62,7 +61,7 @@
         List<DocResult> result = searcher.doQuery(request.getParameter("q"));
         RestApiServlet.replyJson(req, rsp, ImmutableListMultimap.of(), result);
       } catch (DocQueryException e) {
-        log.error("Doc search failed:", e);
+        logger.atSevere().withCause(e).log("Doc search failed");
         rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       }
     } else {
diff --git a/java/com/google/gerrit/httpd/RunAsFilter.java b/java/com/google/gerrit/httpd/RunAsFilter.java
index 9940cd9..f3bf5af 100644
--- a/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -18,6 +18,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -41,13 +42,11 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Allows running a request as another user account. */
 @Singleton
 class RunAsFilter implements Filter {
-  private static final Logger log = LoggerFactory.getLogger(RunAsFilter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final String RUN_AS = "X-Gerrit-RunAs";
 
   static class Module extends ServletModule {
@@ -99,7 +98,7 @@
         replyError(req, res, SC_FORBIDDEN, "not permitted to use " + RUN_AS, null);
         return;
       } catch (PermissionBackendException e) {
-        log.warn("cannot check runAs", e);
+        logger.atWarning().withCause(e).log("cannot check runAs");
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, RUN_AS + " unavailable", null);
         return;
       }
@@ -108,7 +107,7 @@
       try {
         target = accountResolver.find(runas);
       } catch (OrmException | IOException | ConfigInvalidException e) {
-        log.warn("cannot resolve account for " + RUN_AS, e);
+        logger.atWarning().withCause(e).log("cannot resolve account for %s", RUN_AS);
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
         return;
       }
diff --git a/java/com/google/gerrit/httpd/UrlModule.java b/java/com/google/gerrit/httpd/UrlModule.java
index 6210385..1702a38 100644
--- a/java/com/google/gerrit/httpd/UrlModule.java
+++ b/java/com/google/gerrit/httpd/UrlModule.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritOptions;
-import com.google.gwtexpui.server.CacheControlFilter;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.internal.UniqueAnnotations;
@@ -56,8 +55,7 @@
 
   @Override
   protected void configureServlets() {
-    filter("/*").through(Key.get(CacheControlFilter.class));
-    bind(Key.get(CacheControlFilter.class)).in(SINGLETON);
+    filter("/*").through(GwtCacheControlFilter.class);
 
     if (options.enableGwtUi()) {
       filter("/").through(XsrfCookieFilter.class);
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index 8b6694c..457e65f 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -29,6 +29,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.cache.Cache;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -43,11 +44,9 @@
 import java.security.SecureRandom;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class WebSessionManager {
-  private static final Logger log = LoggerFactory.getLogger(WebSessionManager.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   public static final String CACHE_NAME = "web_sessions";
 
   private final long sessionMaxAgeMillis;
@@ -69,10 +68,9 @@
                 SECONDS.convert(MAX_AGE_MINUTES, MINUTES),
                 SECONDS));
     if (sessionMaxAgeMillis < MINUTES.toMillis(5)) {
-      log.warn(
-          String.format(
-              "cache.%s.maxAge is set to %d milliseconds; it should be at least 5 minutes.",
-              CACHE_NAME, sessionMaxAgeMillis));
+      logger.atWarning().log(
+          "cache.%s.maxAge is set to %d milliseconds; it should be at least 5 minutes.",
+          CACHE_NAME, sessionMaxAgeMillis);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 77c79bb..ea01809 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -34,7 +34,7 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index c7229bc..0b3c29d 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index d86c85a..fd2f628 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.CanonicalWebUrl;
@@ -29,7 +30,7 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -40,8 +41,6 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -56,7 +55,7 @@
 @Singleton
 class HttpLoginServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private static final Logger log = LoggerFactory.getLogger(HttpLoginServlet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicItem<WebSession> webSession;
   private final CanonicalWebUrl urlProvider;
@@ -86,10 +85,10 @@
     CacheHeaders.setNotCacheable(rsp);
     final String user = authFilter.getRemoteUser(req);
     if (user == null || "".equals(user)) {
-      log.error(
-          "Unable to authenticate user by "
-              + authFilter.getLoginHeader()
-              + " request header.  Check container or server configuration.");
+      logger.atSevere().log(
+          "Unable to authenticate user by %s request header."
+              + " Check container or server configuration.",
+          authFilter.getLoginHeader());
 
       final Document doc =
           HtmlDomUtil.parseFile( //
@@ -118,7 +117,7 @@
     try {
       arsp = accountManager.authenticate(areq);
     } catch (AccountException e) {
-      log.error("Unable to authenticate user \"" + user + "\"", e);
+      logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user);
       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
       return;
     }
@@ -126,16 +125,12 @@
     String remoteExternalId = authFilter.getRemoteExternalIdToken(req);
     if (remoteExternalId != null) {
       try {
-        log.debug("Associating external identity \"{}\" to user \"{}\"", remoteExternalId, user);
+        logger.atFine().log(
+            "Associating external identity \"%s\" to user \"%s\"", remoteExternalId, user);
         updateRemoteExternalId(arsp, remoteExternalId);
       } catch (AccountException | OrmException | ConfigInvalidException e) {
-        log.error(
-            "Unable to associate external identity \""
-                + remoteExternalId
-                + "\" to user \""
-                + user
-                + "\"",
-            e);
+        logger.atSevere().withCause(e).log(
+            "Unable to associate external identity \"%s\" to user \"%s\"", remoteExternalId, user);
         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
         return;
       }
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index 534e50ec..40807c0 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.auth.container;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
@@ -32,14 +33,12 @@
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class HttpsClientSslCertAuthFilter implements Filter {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final Pattern REGEX_USERID = Pattern.compile("CN=([^,]*)");
-  private static final Logger log = LoggerFactory.getLogger(HttpsClientSslCertAuthFilter.class);
 
   private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
@@ -77,7 +76,7 @@
       arsp = accountManager.authenticate(areq);
     } catch (AccountException e) {
       String err = "Unable to authenticate user \"" + userName + "\"";
-      log.error(err, e);
+      logger.atSevere().withCause(e).log(err);
       throw new ServletException(err, e);
     }
     webSession.get().login(arsp, true);
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
index e93b0b6..f21c96e 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index 4671475..116ad6d 100644
--- a/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.CanonicalWebUrl;
@@ -32,7 +33,7 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.AuthenticationFailedException;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,8 +42,6 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
@@ -50,7 +49,7 @@
 @SuppressWarnings("serial")
 @Singleton
 class LdapLoginServlet extends HttpServlet {
-  private static final Logger log = LoggerFactory.getLogger(LdapLoginServlet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AccountManager accountManager;
   private final DynamicItem<WebSession> webSession;
@@ -130,15 +129,15 @@
     } catch (AuthenticationFailedException e) {
       // This exception is thrown if the user provided wrong credentials, we don't need to log a
       // stacktrace for it.
-      log.warn("'{}' failed to sign in: {}", username, e.getMessage());
+      logger.atWarning().log("'%s' failed to sign in: %s", username, e.getMessage());
       sendForm(req, res, "Invalid username or password.");
       return;
     } catch (AccountException e) {
-      log.warn("'{}' failed to sign in", username, e);
+      logger.atWarning().withCause(e).log("'%s' failed to sign in", username);
       sendForm(req, res, "Authentication failed.");
       return;
     } catch (RuntimeException e) {
-      log.error("LDAP authentication failed", e);
+      logger.atSevere().withCause(e).log("LDAP authentication failed");
       sendForm(req, res, "Authentication unavailable at this time.");
       return;
     }
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index aa63f0d..96726ad 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -15,9 +15,9 @@
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/commons:codec",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index 68b28a9d..b780fa0 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
@@ -47,13 +48,12 @@
 import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @SessionScoped
 /* OAuth protocol implementation */
 class OAuthSession {
-  private static final Logger log = LoggerFactory.getLogger(OAuthSession.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final SecureRandom randomState = newRandomGenerator();
   private final String state;
   private final DynamicItem<WebSession> webSession;
@@ -93,7 +93,7 @@
   boolean login(
       HttpServletRequest request, HttpServletResponse response, OAuthServiceProvider oauth)
       throws IOException {
-    log.debug("Login " + this);
+    logger.atFine().log("Login %s", this);
 
     if (isOAuthFinal(request)) {
       if (!checkState(request)) {
@@ -101,19 +101,19 @@
         return false;
       }
 
-      log.debug("Login-Retrieve-User " + this);
+      logger.atFine().log("Login-Retrieve-User %s", this);
       OAuthToken token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
       user = oauth.getUserInfo(token);
 
       if (isLoggedIn()) {
-        log.debug("Login-SUCCESS " + this);
+        logger.atFine().log("Login-SUCCESS %s", this);
         authenticateAndRedirect(request, response, token);
         return true;
       }
       response.sendError(SC_UNAUTHORIZED);
       return false;
     }
-    log.debug("Login-PHASE1 " + this);
+    logger.atFine().log("Login-PHASE1 %s", this);
     redirectToken = request.getRequestURI();
     // We are here in content of filter.
     // Due to this Jetty limitation:
@@ -148,7 +148,7 @@
       accountId = arsp.getAccountId();
       tokenCache.put(accountId, token);
     } catch (AccountException e) {
-      log.error("Unable to authenticate user \"" + user + "\"", e);
+      logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user);
       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
       return;
     }
@@ -169,40 +169,29 @@
     if (claimedId.isPresent() && actualId.isPresent()) {
       if (claimedId.get().equals(actualId.get())) {
         // Both link to the same account, that's what we expected.
-        log.debug("OAuth2: claimed identity equals current id");
+        logger.atFine().log("OAuth2: claimed identity equals current id");
       } else {
         // This is (for now) a fatal error. There are two records
         // for what might be the same user.
         //
-        log.error(
+        logger.atSevere().log(
             "OAuth accounts disagree over user identity:\n"
-                + "  Claimed ID: "
-                + claimedId.get()
-                + " is "
-                + claimedIdentifier
-                + "\n"
-                + "  Delgate ID: "
-                + actualId.get()
-                + " is "
-                + user.getExternalId());
+                + "  Claimed ID: %s is %s\n"
+                + "  Delgate ID: %s is %s",
+            claimedId.get(), claimedIdentifier, actualId.get(), user.getExternalId());
         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
         return false;
       }
     } else if (claimedId.isPresent() && !actualId.isPresent()) {
       // Claimed account already exists: link to it.
       //
-      log.info("OAuth2: linking claimed identity to {}", claimedId.get().toString());
+      logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get().toString());
       try {
         accountManager.link(claimedId.get(), req);
       } catch (OrmException | ConfigInvalidException e) {
-        log.error(
-            "Cannot link: "
-                + user.getExternalId()
-                + " to user identity:\n"
-                + "  Claimed ID: "
-                + claimedId.get()
-                + " is "
-                + claimedIdentifier);
+        logger.atSevere().log(
+            "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
+            user.getExternalId(), claimedId.get(), claimedIdentifier);
         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
         return false;
       }
@@ -215,11 +204,9 @@
     try {
       accountManager.link(identifiedUser.get().getAccountId(), areq);
     } catch (OrmException | ConfigInvalidException e) {
-      log.error(
-          "Cannot link: "
-              + user.getExternalId()
-              + " to user identity: "
-              + identifiedUser.get().getAccountId());
+      logger.atSevere().log(
+          "Cannot link: %s to user identity: %s",
+          user.getExternalId(), identifiedUser.get().getAccountId());
       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
       return false;
     } finally {
@@ -241,7 +228,7 @@
   private boolean checkState(ServletRequest request) {
     String s = Strings.nullToEmpty(request.getParameter("state"));
     if (!s.equals(state)) {
-      log.error("Illegal request state '" + s + "' on OAuthProtocol " + this);
+      logger.atSevere().log("Illegal request state '%s' on OAuthProtocol %s", s, this);
       return false;
     }
     return true;
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index 44b7bd1..bfb2551 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -11,16 +11,16 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/reviewdb:server",
-        "//java/com/google/gwtexpui/server",
+        "//java/com/google/gerrit/util/http",
         "//java/com/google/gerrit/server",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
         "//lib/commons:codec",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/openid:consumer",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 6090fed..adf6458 100644
--- a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
@@ -47,8 +48,6 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
@@ -56,7 +55,8 @@
 @SuppressWarnings("serial")
 @Singleton
 class LoginForm extends HttpServlet {
-  private static final Logger log = LoggerFactory.getLogger(LoginForm.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final ImmutableMap<String, String> ALL_PROVIDERS =
       ImmutableMap.of(
           "launchpad", OpenIdUrls.URL_LAUNCHPAD,
@@ -91,7 +91,7 @@
     this.oauthServiceProviders = oauthServiceProviders;
 
     if (urlProvider == null || Strings.isNullOrEmpty(urlProvider.get())) {
-      log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
+      logger.atSevere().log("gerrit.canonicalWebUrl must be set in gerrit.config");
     }
 
     if (authConfig.getAuthType() == AuthType.OPENID_SSO) {
@@ -160,14 +160,14 @@
       mode = SignInMode.SIGN_IN;
     }
 
-    log.debug("mode \"{}\"", mode);
+    logger.atFine().log("mode \"%s\"", mode);
     OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id);
 
     if (oauthProvider == null) {
-      log.debug("OpenId provider \"{}\"", id);
+      logger.atFine().log("OpenId provider \"%s\"", id);
       discover(req, res, link, id, remember, token, mode);
     } else {
-      log.debug("OAuth provider \"{}\"", id);
+      logger.atFine().log("OAuth provider \"%s\"", id);
       OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
       if (!currentUserProvider.get().isIdentifiedUser() && oauthSession.isLoggedIn()) {
         oauthSession.logout();
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index 878f9ee..a1a6715 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -17,6 +17,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
@@ -45,14 +46,13 @@
 import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** OAuth protocol implementation */
 @SessionScoped
 class OAuthSessionOverOpenID {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   static final String GERRIT_LOGIN = "/login";
-  private static final Logger log = LoggerFactory.getLogger(OAuthSessionOverOpenID.class);
   private static final SecureRandom randomState = newRandomGenerator();
   private final String state;
   private final DynamicItem<WebSession> webSession;
@@ -89,7 +89,7 @@
   boolean login(
       HttpServletRequest request, HttpServletResponse response, OAuthServiceProvider oauth)
       throws IOException {
-    log.debug("Login " + this);
+    logger.atFine().log("Login %s", this);
 
     if (isOAuthFinal(request)) {
       if (!checkState(request)) {
@@ -97,19 +97,19 @@
         return false;
       }
 
-      log.debug("Login-Retrieve-User " + this);
+      logger.atFine().log("Login-Retrieve-User %s", this);
       token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
       user = oauth.getUserInfo(token);
 
       if (isLoggedIn()) {
-        log.debug("Login-SUCCESS " + this);
+        logger.atFine().log("Login-SUCCESS %s", this);
         authenticateAndRedirect(request, response);
         return true;
       }
       response.sendError(SC_UNAUTHORIZED);
       return false;
     }
-    log.debug("Login-PHASE1 " + this);
+    logger.atFine().log("Login-PHASE1 %s", this);
     redirectToken = LoginUrlToken.getToken(request);
     response.sendRedirect(oauth.getAuthorizationUrl() + "&state=" + state);
     return false;
@@ -135,50 +135,39 @@
       if (!Strings.isNullOrEmpty(claimedIdentifier)) {
         claimedId = accountManager.lookup(claimedIdentifier);
         if (!claimedId.isPresent()) {
-          log.debug("Claimed identity is unknown");
+          logger.atFine().log("Claimed identity is unknown");
         }
       }
 
       // Use case 1: claimed identity was provided during handshake phase
       // and user account exists for this identity
       if (claimedId.isPresent()) {
-        log.debug("Claimed identity is set and is known");
+        logger.atFine().log("Claimed identity is set and is known");
         if (actualId.isPresent()) {
           if (claimedId.get().equals(actualId.get())) {
             // Both link to the same account, that's what we expected.
-            log.debug("Both link to the same account. All is fine.");
+            logger.atFine().log("Both link to the same account. All is fine.");
           } else {
             // This is (for now) a fatal error. There are two records
             // for what might be the same user. The admin would have to
             // link the accounts manually.
-            log.error(
+            logger.atFine().log(
                 "OAuth accounts disagree over user identity:\n"
-                    + "  Claimed ID: "
-                    + claimedId.get()
-                    + " is "
-                    + claimedIdentifier
-                    + "\n"
-                    + "  Delgate ID: "
-                    + actualId.get()
-                    + " is "
-                    + user.getExternalId());
+                    + "  Claimed ID: %s is %s\n"
+                    + "  Delgate ID: %s is %s",
+                claimedId.get(), claimedIdentifier, actualId.get(), user.getExternalId());
             rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
             return;
           }
         } else {
           // Claimed account already exists: link to it.
-          log.debug("Claimed account already exists: link to it.");
+          logger.atFine().log("Claimed account already exists: link to it.");
           try {
             accountManager.link(claimedId.get(), areq);
           } catch (OrmException | ConfigInvalidException e) {
-            log.error(
-                "Cannot link: "
-                    + user.getExternalId()
-                    + " to user identity:\n"
-                    + "  Claimed ID: "
-                    + claimedId.get()
-                    + " is "
-                    + claimedIdentifier);
+            logger.atSevere().log(
+                "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
+                user.getExternalId(), claimedId.get(), claimedIdentifier);
             rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
             return;
           }
@@ -187,10 +176,11 @@
         // Use case 2: link mode activated from the UI
         Account.Id accountId = identifiedUser.get().getAccountId();
         try {
-          log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(), accountId);
+          logger.atFine().log("Linking \"%s\" to \"%s\"", user.getExternalId(), accountId);
           accountManager.link(accountId, areq);
         } catch (OrmException | ConfigInvalidException e) {
-          log.error("Cannot link: " + user.getExternalId() + " to user identity: " + accountId);
+          logger.atSevere().log(
+              "Cannot link: %s to user identity: %s", user.getExternalId(), accountId);
           rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
           return;
         } finally {
@@ -202,7 +192,7 @@
       areq.setDisplayName(user.getDisplayName());
       arsp = accountManager.authenticate(areq);
     } catch (AccountException e) {
-      log.error("Unable to authenticate user \"" + user + "\"", e);
+      logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user);
       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
       return;
     }
@@ -223,7 +213,7 @@
   private boolean checkState(ServletRequest request) {
     String s = Strings.nullToEmpty(request.getParameter("state"));
     if (!s.equals(state)) {
-      log.error("Illegal request state '" + s + "' on OAuthProtocol " + this);
+      logger.atSevere().log("Illegal request state '%s' on OAuthProtocol %s", s, this);
       return false;
     }
     return true;
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
index a97e8ae..23cf468 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index a971fc3..28256cf 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -64,12 +65,10 @@
 import org.openid4java.message.sreg.SRegRequest;
 import org.openid4java.message.sreg.SRegResponse;
 import org.openid4java.util.HttpClientFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class OpenIdServiceImpl {
-  private static final Logger log = LoggerFactory.getLogger(OpenIdServiceImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final String RETURN_URL = "OpenID";
 
@@ -151,7 +150,7 @@
     final AuthRequest aReq;
     try {
       aReq = manager.authenticate(state.discovered, state.retTo.toString());
-      log.debug("OpenID: openid-realm={}", state.contextUrl);
+      logger.atFine().log("OpenID: openid-realm=%s", state.contextUrl);
       aReq.setRealm(state.contextUrl);
 
       if (requestRegistration(aReq)) {
@@ -173,7 +172,7 @@
         aReq.addExtension(pape);
       }
     } catch (MessageException | ConsumerException e) {
-      log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
+      logger.atSevere().withCause(e).log("Cannot create OpenID redirect for %s" + openidIdentifier);
       return new DiscoveryResult(DiscoveryResult.Status.ERROR);
     }
 
@@ -195,7 +194,7 @@
     try {
       return accountManager.lookup(aReq.getIdentity()) == null;
     } catch (AccountException e) {
-      log.warn("Cannot determine if user account exists", e);
+      logger.atWarning().withCause(e).log("Cannot determine if user account exists");
       return true;
     }
   }
@@ -250,17 +249,16 @@
       if ("Nonce verification failed.".equals(result.getStatusMsg())) {
         // We might be suffering from clock skew on this system.
         //
-        log.error(
-            "OpenID failure: "
-                + result.getStatusMsg()
-                + "  Likely caused by clock skew on this server,"
-                + " install/configure NTP.");
+        logger.atSevere().log(
+            "OpenID failure: %s  Likely caused by clock skew on this server,"
+                + " install/configure NTP.",
+            result.getStatusMsg());
         cancelWithError(req, rsp, result.getStatusMsg());
 
       } else if (result.getStatusMsg() != null) {
         // Authentication failed.
         //
-        log.error("OpenID failure: " + result.getStatusMsg());
+        logger.atSevere().log("OpenID failure: %s", result.getStatusMsg());
         cancelWithError(req, rsp, result.getStatusMsg());
 
       } else {
@@ -286,12 +284,12 @@
         // right now. Instead of blocking all of them log the error and
         // let the authentication complete anyway.
         //
-        log.error("Invalid PAPE response " + openidIdentifier + ": " + err);
+        logger.atSevere().log("Invalid PAPE response %s: %s", openidIdentifier, err);
         unsupported = true;
         ext = null;
       }
       if (!unsupported && ext == null) {
-        log.error("No PAPE extension response from " + openidIdentifier);
+        logger.atSevere().log("No PAPE extension response from %s", openidIdentifier);
         cancelWithError(req, rsp, "OpenID provider does not support PAPE.");
         return;
       }
@@ -354,7 +352,7 @@
         }
 
         if (!match) {
-          log.error("Domain disallowed: " + emailDomain);
+          logger.atSevere().log("Domain disallowed: %s", emailDomain);
           cancelWithError(req, rsp, "Domain disallowed");
           return;
         }
@@ -376,17 +374,11 @@
           // This is (for now) a fatal error. There are two records
           // for what might be the same user.
           //
-          log.error(
+          logger.atSevere().log(
               "OpenID accounts disagree over user identity:\n"
-                  + "  Claimed ID: "
-                  + claimedId.get()
-                  + " is "
-                  + claimedIdentifier
-                  + "\n"
-                  + "  Delgate ID: "
-                  + actualId.get()
-                  + " is "
-                  + areq.getExternalIdKey());
+                  + "  Claimed ID: %s is %s\n"
+                  + "  Delgate ID: %s is %s",
+              claimedId.get(), claimedIdentifier, actualId.get(), areq.getExternalIdKey());
           cancelWithError(req, rsp, "Contact site administrator");
           return;
         }
@@ -451,7 +443,7 @@
           }
       }
     } catch (AccountException e) {
-      log.error("OpenID authentication failure", e);
+      logger.atSevere().withCause(e).log("OpenID authentication failure");
       cancelWithError(req, rsp, "Contact site administrator");
     }
   }
@@ -531,7 +523,7 @@
     try {
       list = manager.discover(openidIdentifier);
     } catch (DiscoveryException e) {
-      log.error("Cannot discover OpenID " + openidIdentifier, e);
+      logger.atSevere().withCause(e).log("Cannot discover OpenID %s", openidIdentifier);
       return null;
     }
     if (list == null || list.isEmpty()) {
diff --git a/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java b/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
index af853cc..d57e629 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
@@ -18,7 +18,7 @@
 
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.server.config.GitwebCgiConfig;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
index 5e22081..feee3ba 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
index 651b582..82dd901 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
@@ -18,7 +18,7 @@
 
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.server.config.GitwebCgiConfig;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index cc22d24..5e59c9a 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -34,6 +34,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Splitter;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -54,7 +55,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -85,14 +86,12 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Invokes {@code gitweb.cgi} for the project given in {@code p}. */
 @SuppressWarnings("serial")
 @Singleton
 class GitwebServlet extends HttpServlet {
-  private static final Logger log = LoggerFactory.getLogger(GitwebServlet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String PROJECT_LIST_ACTION = "project_list";
 
@@ -137,7 +136,7 @@
       try {
         uri = new URI(url);
       } catch (URISyntaxException e) {
-        log.error("Invalid gitweb.url: " + url);
+        logger.atSevere().log("Invalid gitweb.url: %s", url);
       }
       gitwebUrl = uri;
     } else {
@@ -428,7 +427,7 @@
       sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
       return;
     } catch (IOException | PermissionBackendException err) {
-      log.error("cannot load " + name, err);
+      logger.atSevere().withCause(err).log("cannot load %s", name);
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       return;
     } catch (ResourceConflictException e) {
@@ -528,13 +527,13 @@
 
       final int status = proc.exitValue();
       if (0 != status) {
-        log.error("Non-zero exit status (" + status + ") from " + gitwebCgi);
+        logger.atSevere().log("Non-zero exit status (%d) from %s", status, gitwebCgi);
         if (!rsp.isCommitted()) {
           rsp.sendError(500);
         }
       }
     } catch (InterruptedException ie) {
-      log.debug("CGI: interrupted waiting for CGI to terminate");
+      logger.atFine().log("CGI: interrupted waiting for CGI to terminate");
     }
   }
 
@@ -659,7 +658,7 @@
                   dst.close();
                 }
               } catch (IOException e) {
-                log.error("Unexpected error copying input to CGI", e);
+                logger.atSevere().withCause(e).log("Unexpected error copying input to CGI");
               }
             },
             "Gitweb-InputFeeder")
@@ -679,9 +678,9 @@
                   }
                   b.append("CGI: ").append(line);
                 }
-                log.error(b.toString());
+                logger.atSevere().log(b.toString());
               } catch (IOException e) {
-                log.error("Unexpected error copying stderr from CGI", e);
+                logger.atSevere().withCause(e).log("Unexpected error copying stderr from CGI");
               }
             },
             "Gitweb-ErrorLogger")
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index f240088..292ceff 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -27,10 +27,10 @@
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//prolog:gerrit-prolog-common",
     ],
 )
diff --git a/java/com/google/gerrit/httpd/init/SiteInitializer.java b/java/com/google/gerrit/httpd/init/SiteInitializer.java
index 17a95b5..de4f284 100644
--- a/java/com/google/gerrit/httpd/init/SiteInitializer.java
+++ b/java/com/google/gerrit/httpd/init/SiteInitializer.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.init;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.pgm.init.BaseInit;
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import java.nio.file.Path;
@@ -23,11 +24,9 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public final class SiteInitializer {
-  private static final Logger LOG = LoggerFactory.getLogger(SiteInitializer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final String sitePath;
   private final String initPath;
@@ -49,7 +48,7 @@
     try {
       if (sitePath != null) {
         Path site = Paths.get(sitePath);
-        LOG.info("Initializing site at " + site.toRealPath().normalize());
+        logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
         new BaseInit(site, false, true, pluginsDistribution, pluginsToInstall).run();
         return;
       }
@@ -60,7 +59,7 @@
           site = Paths.get(initPath);
         }
         if (site != null) {
-          LOG.info("Initializing site at " + site.toRealPath().normalize());
+          logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
           new BaseInit(
                   site,
                   new ReviewDbDataSourceProvider(),
@@ -72,7 +71,7 @@
         }
       }
     } catch (Exception e) {
-      LOG.error("Site init failed", e);
+      logger.atSevere().withCause(e).log("Site init failed");
       throw new RuntimeException(e);
     }
   }
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 690d1ac..9ce7690 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -18,6 +18,7 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.common.base.Splitter;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
@@ -77,10 +78,8 @@
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
-import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
 import com.google.gerrit.server.restapi.RestApiModule;
-import com.google.gerrit.server.restapi.config.RestCacheAdminModule;
 import com.google.gerrit.server.schema.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
@@ -127,12 +126,10 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Configures the web application environment for Gerrit Code Review. */
 public class WebAppInitializer extends GuiceServletContextListener implements Filter {
-  private static final Logger log = LoggerFactory.getLogger(WebAppInitializer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private Path sitePath;
   private Injector dbInjector;
@@ -194,7 +191,7 @@
           buf.append("\nResolve above errors before continuing.");
           buf.append("\nComplete stack trace follows:");
         }
-        log.error(buf.toString(), first.getCause());
+        logger.atSevere().withCause(first.getCause()).log(buf.toString());
         throw new CreationException(Collections.singleton(first));
       }
 
@@ -358,10 +355,8 @@
     // with the proper classes (e.g. group backends, custom Prolog
     // predicates) and the associated rules ready to be evaluated.
     modules.add(new PluginModule());
-    modules.add(new PluginRestApiModule());
 
     modules.add(new RestApiModule());
-    modules.add(new RestCacheAdminModule());
     modules.add(new GpgModule(config));
     modules.add(new StartupChecks.Module());
 
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index b490810..9a24e47 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -32,6 +32,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteStreams;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -51,8 +52,8 @@
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gerrit.util.http.RequestUtil;
-import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -88,17 +89,17 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class HttpPluginServlet extends HttpServlet implements StartPluginListener, ReloadPluginListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final int SMALL_RESOURCE = 128 * 1024;
   private static final long serialVersionUID = 1L;
-  private static final Logger log = LoggerFactory.getLogger(HttpPluginServlet.class);
 
   private final MimeUtilFileTypeRegistry mimeUtil;
   private final Provider<String> webUrl;
@@ -190,7 +191,7 @@
       try {
         filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
       } catch (RuntimeException e) {
-        log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
+        logger.atWarning().withCause(e).log("Plugin %s cannot load GuiceFilter", name);
         return null;
       }
 
@@ -198,7 +199,7 @@
         ServletContext ctx = PluginServletContext.create(plugin, wrapper.getFullPath(name));
         filter.init(new WrappedFilterConfig(ctx));
       } catch (ServletException e) {
-        log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
+        logger.atWarning().withCause(e).log("Plugin %s failed to initialize HTTP", name);
         return null;
       }
 
@@ -422,11 +423,9 @@
               && (name.endsWith(".md") || name.endsWith(".html"))
               && size.isPresent()) {
             if (size.get() <= 0 || size.get() > SMALL_RESOURCE) {
-              log.warn(
-                  String.format(
-                      "Plugin %s: %s omitted from document index. "
-                          + "Size %d out of range (0,%d).",
-                      pluginName, name.substring(prefix.length()), size.get(), SMALL_RESOURCE));
+              logger.atWarning().log(
+                  "Plugin %s: %s omitted from document index. " + "Size %d out of range (0,%d).",
+                  pluginName, name.substring(prefix.length()), size.get(), SMALL_RESOURCE);
               return false;
             }
             return true;
@@ -448,10 +447,9 @@
         if (about == null) {
           about = entry;
         } else {
-          log.warn(
-              String.format(
-                  "Plugin %s: Multiple 'about' documents found; using %s",
-                  pluginName, about.getName().substring(prefix.length())));
+          logger.atWarning().log(
+              "Plugin %s: Multiple 'about' documents found; using %s",
+              pluginName, about.getName().substring(prefix.length()));
         }
       } else {
         docs.add(entry);
@@ -472,7 +470,7 @@
       try (BufferedReader reader = new BufferedReader(isr)) {
         String line;
         while ((line = reader.readLine()) != null) {
-          line = line.trim();
+          line = StringUtils.stripEnd(line, null);
           if (line.isEmpty()) {
             aboutContent.append("\n");
           } else {
@@ -729,9 +727,8 @@
         }
         return def;
       } catch (IOException e) {
-        log.warn(
-            String.format("Error getting %s for plugin %s, using default", attr, plugin.getName()),
-            e);
+        logger.atWarning().withCause(e).log(
+            "Error getting %s for plugin %s, using default", attr, plugin.getName());
         return null;
       }
     }
diff --git a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
index e8f2e33..67ee3ba 100644
--- a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -18,13 +18,14 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.GuiceFilter;
@@ -45,14 +46,14 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class LfsPluginServlet extends HttpServlet
     implements StartPluginListener, ReloadPluginListener {
   private static final long serialVersionUID = 1L;
-  private static final Logger log = LoggerFactory.getLogger(LfsPluginServlet.class);
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String MESSAGE_LFS_NOT_CONFIGURED =
       "{\"message\":\"No LFS plugin is configured to handle LFS requests.\"}";
 
@@ -139,7 +140,7 @@
       try {
         guiceFilter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
       } catch (RuntimeException e) {
-        log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
+        logger.atWarning().withCause(e).log("Plugin %s cannot load GuiceFilter", name);
         return null;
       }
 
@@ -147,7 +148,7 @@
         ServletContext ctx = PluginServletContext.create(plugin, "/");
         guiceFilter.init(new WrappedFilterConfig(ctx));
       } catch (ServletException e) {
-        log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
+        logger.atWarning().withCause(e).log("Plugin %s failed to initialize HTTP", name);
         return null;
       }
 
diff --git a/java/com/google/gerrit/httpd/plugins/PluginServletContext.java b/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
index 53b49a4..6a8ef32 100644
--- a/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
+++ b/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.plugins;
 
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.server.plugins.Plugin;
 import java.io.InputStream;
@@ -29,11 +30,9 @@
 import javax.servlet.RequestDispatcher;
 import javax.servlet.Servlet;
 import javax.servlet.ServletContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class PluginServletContext {
-  private static final Logger log = LoggerFactory.getLogger(PluginServletContext.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static ServletContext create(Plugin plugin, String contextPath) {
     return (ServletContext)
@@ -155,7 +154,7 @@
 
     @Override
     public void log(String msg, Throwable reason) {
-      log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
+      logger.atWarning().withCause(reason).log("[plugin %s] %s", plugin.getName(), msg);
     }
 
     @Override
diff --git a/java/com/google/gerrit/httpd/raw/BazelBuild.java b/java/com/google/gerrit/httpd/raw/BazelBuild.java
index f52792c..940a51b 100644
--- a/java/com/google/gerrit/httpd/raw/BazelBuild.java
+++ b/java/com/google/gerrit/httpd/raw/BazelBuild.java
@@ -19,10 +19,11 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.escape.Escaper;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.html.HtmlEscapers;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
@@ -33,11 +34,9 @@
 import java.util.Properties;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class BazelBuild {
-  private static final Logger log = LoggerFactory.getLogger(BazelBuild.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Path sourceRoot;
 
@@ -49,7 +48,7 @@
   public void build(Label label) throws IOException, BuildFailureException {
     ProcessBuilder proc = newBuildProcess(label);
     proc.directory(sourceRoot.toFile()).redirectErrorStream(true);
-    log.info("building " + label.fullName());
+    logger.atInfo().log("building %s", label.fullName());
     long start = TimeUtil.nowMs();
     Process rebuild = proc.start();
     byte[] out;
@@ -67,12 +66,12 @@
           "interrupted waiting for: " + Joiner.on(' ').join(proc.command()));
     }
     if (status != 0) {
-      log.warn("build failed: " + new String(out, UTF_8));
+      logger.atWarning().log("build failed: %s", new String(out, UTF_8));
       throw new BuildFailureException(out);
     }
 
     long time = TimeUtil.nowMs() - start;
-    log.info(String.format("UPDATED    %s in %.3fs", label.fullName(), time / 1000.0));
+    logger.atInfo().log("UPDATED    %s in %.3fs", label.fullName(), time / 1000.0);
   }
 
   // Represents a label in bazel.
diff --git a/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index ffecf1b..160732e 100644
--- a/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.common.primitives.Bytes;
@@ -38,7 +39,7 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.GetDiffPreferences;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtjsonrpc.server.JsonServlet;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
@@ -60,8 +61,6 @@
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -70,7 +69,7 @@
 @SuppressWarnings("serial")
 @Singleton
 public class HostPageServlet extends HttpServlet {
-  private static final Logger log = LoggerFactory.getLogger(HostPageServlet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String HPD_ID = "gerrit_hostpagedata";
   private static final int DEFAULT_JS_LOAD_TIMEOUT = 5000;
@@ -141,7 +140,7 @@
         }
         src += "?content=" + md.hash().toString();
       } else {
-        log.debug("No " + src + " in webapp root; keeping noncache.js URL");
+        logger.atFine().log("No %s in webapp root; keeping noncache.js URL", src);
       }
     } catch (IOException e) {
       throw new IOException("Failed reading " + src, e);
@@ -173,7 +172,7 @@
         page = p;
       }
     } catch (IOException e) {
-      log.error("Cannot refresh site header/footer", e);
+      logger.atSevere().withCause(e).log("Cannot refresh site header/footer");
     }
     return p;
   }
@@ -225,7 +224,7 @@
         | ConfigInvalidException
         | IOException
         | PermissionBackendException e) {
-      log.warn("Cannot query account diff preferences", e);
+      logger.atWarning().withCause(e).log("Cannot query account diff preferences");
     }
     return DiffPreferencesInfo.defaults();
   }
diff --git a/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java b/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
index 10735a5..e12f0a5 100644
--- a/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
+++ b/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
index 90aedbe..c6c3367 100644
--- a/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
+++ b/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.raw;
 
-import com.google.gwtexpui.linker.server.UserAgentRule;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
diff --git a/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 94ee221..64b5bbb 100644
--- a/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -30,10 +30,11 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -47,8 +48,6 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Base class for serving static resources.
@@ -58,7 +57,7 @@
 public abstract class ResourceServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
 
-  private static final Logger log = LoggerFactory.getLogger(ResourceServlet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final int CACHE_FILE_SIZE_LIMIT_BYTES = 100 << 10;
 
@@ -161,7 +160,7 @@
         r = cache.get(p, newLoader(p));
       }
     } catch (ExecutionException e) {
-      log.warn("Cannot load static resource " + req.getPathInfo(), e);
+      logger.atWarning().withCause(e).log("Cannot load static resource %s", req.getPathInfo());
       CacheHeaders.setNotCacheable(rsp);
       rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
       return;
@@ -214,12 +213,12 @@
     try {
       Path p = getResourcePath(name);
       if (p == null) {
-        log.warn(String.format("Path doesn't exist %s", name));
+        logger.atWarning().log("Path doesn't exist %s", name);
         return null;
       }
       return cache.get(p, newLoader(p));
     } catch (ExecutionException | IOException e) {
-      log.warn(String.format("Cannot load static resource %s", name), e);
+      logger.atWarning().withCause(e).log("Cannot load static resource %s", name);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/httpd/raw/SshInfoServlet.java b/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
index 55bc2a6..1d1fe6cc 100644
--- a/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
+++ b/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
@@ -17,7 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.jcraft.jsch.HostKey;
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 915e9ed..06ec799 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -21,6 +21,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.UiType;
 import com.google.gerrit.httpd.XsrfCookieFilter;
@@ -57,11 +58,9 @@
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class StaticModule extends ServletModule {
-  private static final Logger log = LoggerFactory.getLogger(StaticModule.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String CACHE = "static_content";
   public static final String GERRIT_UI_COOKIE = "GERRIT_UI";
@@ -184,7 +183,7 @@
         if (exists(configPath) && isReadable(configPath)) {
           return new SingleFileServlet(cache, configPath, true);
         }
-        log.warn("Cannot read httpd.robotsFile, using default");
+        logger.atWarning().log("Cannot read httpd.robotsFile, using default");
       }
       Paths p = getPaths();
       if (p.warFs != null) {
diff --git a/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/java/com/google/gerrit/httpd/raw/UserAgentRule.java
similarity index 93%
rename from java/com/google/gwtexpui/linker/server/UserAgentRule.java
rename to java/com/google/gerrit/httpd/raw/UserAgentRule.java
index 8f7bede..4aac243 100644
--- a/java/com/google/gwtexpui/linker/server/UserAgentRule.java
+++ b/java/com/google/gerrit/httpd/raw/UserAgentRule.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gwtexpui.linker.server;
+package com.google.gerrit.httpd.raw;
 
 import static java.util.regex.Pattern.compile;
 
@@ -28,15 +28,15 @@
  *
  * <p>Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
  */
-public class UserAgentRule {
+class UserAgentRule {
   private static final Pattern msie = compile(".*msie ([0-11]+)\\.([0-11]+).*");
   private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
 
-  public String getName() {
+  String getName() {
     return "user.agent";
   }
 
-  public String select(HttpServletRequest req) {
+  String select(HttpServletRequest req) {
     String ua = req.getHeader("User-Agent");
     if (ua == null) {
       return null;
diff --git a/java/com/google/gerrit/httpd/resources/Resource.java b/java/com/google/gerrit/httpd/resources/Resource.java
index bfa0b95..878912e 100644
--- a/java/com/google/gerrit/httpd/resources/Resource.java
+++ b/java/com/google/gerrit/httpd/resources/Resource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.resources;
 
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gerrit.util.http.CacheHeaders;
 import java.io.IOException;
 import java.io.Serializable;
 import javax.servlet.http.HttpServletRequest;
diff --git a/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index bfaf0c7..2870cd0 100644
--- a/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -38,11 +38,11 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
-import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 913128e..546bb9f 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -58,6 +58,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.CountingOutputStream;
 import com.google.common.math.IntMath;
@@ -109,6 +110,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gerrit.util.http.RequestUtil;
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
@@ -122,7 +124,6 @@
 import com.google.gson.stream.JsonToken;
 import com.google.gson.stream.JsonWriter;
 import com.google.gson.stream.MalformedJsonException;
-import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
@@ -164,12 +165,11 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.TemporaryBuffer.Heap;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class RestApiServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private static final Logger log = LoggerFactory.getLogger(RestApiServlet.class);
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** MIME type used for a JSON response body. */
   private static final String JSON_TYPE = "application/json";
@@ -846,16 +846,25 @@
     throw new BadRequestException("Expected JSON object");
   }
 
+  @SuppressWarnings("unchecked")
   private static Object createInstance(Type type)
       throws NoSuchMethodException, InstantiationException, IllegalAccessException,
           InvocationTargetException {
     if (type instanceof Class) {
-      @SuppressWarnings("unchecked")
       Class<Object> clazz = (Class<Object>) type;
       Constructor<Object> c = clazz.getDeclaredConstructor();
       c.setAccessible(true);
       return c.newInstance();
     }
+    if (type instanceof ParameterizedType) {
+      Type rawType = ((ParameterizedType) type).getRawType();
+      if (rawType instanceof Class && List.class.isAssignableFrom((Class<Object>) rawType)) {
+        return new ArrayList<>();
+      }
+      if (rawType instanceof Class && Map.class.isAssignableFrom((Class<Object>) rawType)) {
+        return new HashMap<>();
+      }
+    }
     throw new InstantiationException("Cannot make " + type);
   }
 
@@ -1192,7 +1201,7 @@
     if (!Strings.isNullOrEmpty(req.getQueryString())) {
       uri += "?" + req.getQueryString();
     }
-    log.error("Error in {} {}", req.getMethod(), uri, err);
+    logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uri);
 
     if (!res.isCommitted()) {
       res.reset();
diff --git a/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index e787a48..f5d2216 100644
--- a/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
@@ -38,13 +39,12 @@
 import java.lang.reflect.Method;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Base JSON servlet to ensure the current user is not forged. */
 @SuppressWarnings("serial")
 final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> {
-  private static final Logger log = LoggerFactory.getLogger(GerritJsonServlet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final ThreadLocal<GerritCall> currentCall = new ThreadLocal<>();
   private static final ThreadLocal<MethodHandle> currentMethod = new ThreadLocal<>();
   private final DynamicItem<WebSession> session;
@@ -141,7 +141,7 @@
                 result));
       }
     } catch (Throwable all) {
-      log.error("Unable to log the call", all);
+      logger.atSevere().withCause(all).log("Unable to log the call");
     }
   }
 
@@ -190,7 +190,7 @@
         declaredField = clazz.getDeclaredField(fieldName);
         declaredField.setAccessible(true);
       } catch (Exception e) {
-        log.error("Unable to expose RPS/JSON result field");
+        logger.atSevere().log("Unable to expose RPS/JSON result field");
       }
       return declaredField;
     }
@@ -205,9 +205,9 @@
         Method method = (Method) methodField.get(this.getMethod());
         return method.getDeclaringClass();
       } catch (IllegalArgumentException e) {
-        log.error("Cannot access result field");
+        logger.atSevere().log("Cannot access result field");
       } catch (IllegalAccessException e) {
-        log.error("No permissions to access result field");
+        logger.atSevere().log("No permissions to access result field");
       }
 
       return null;
@@ -222,9 +222,9 @@
       try {
         return resultField.get(this);
       } catch (IllegalArgumentException e) {
-        log.error("Cannot access result field");
+        logger.atSevere().log("Cannot access result field");
       } catch (IllegalAccessException e) {
-        log.error("No permissions to access result field");
+        logger.atSevere().log("No permissions to access result field");
       }
 
       return null;
diff --git a/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 7a7713d..634e8d8 100644
--- a/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.data.SystemInfoService;
 import com.google.gerrit.server.ssh.SshInfo;
@@ -26,11 +27,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.http.HttpServletRequest;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class SystemInfoServiceImpl implements SystemInfoService {
-  private static final Logger log = LoggerFactory.getLogger(SystemInfoServiceImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final JSch JSCH = new JSch();
 
@@ -63,7 +62,7 @@
     HttpServletRequest r = httpRequest.get();
     String ua = r.getHeader("User-Agent");
     message = message.replaceAll("\n", "\n  ");
-    log.error("Client UI JavaScript error: User-Agent=" + ua + ": " + message);
+    logger.atSevere().log("Client UI JavaScript error: User-Agent=%s: %s", ua, message);
     callback.onSuccess(VoidResult.INSTANCE);
   }
 }
diff --git a/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java b/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
index dca4d0f..655f4ca 100644
--- a/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
+++ b/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.common.FileUtil.lastModified;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -25,14 +26,12 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
 @Singleton
 public class SiteHeaderFooter {
-  private static final Logger log = LoggerFactory.getLogger(SiteHeaderFooter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final boolean refreshHeaderFooter;
   private final SitePaths sitePaths;
@@ -48,7 +47,7 @@
       t.load();
       template = t;
     } catch (IOException e) {
-      log.warn("Cannot load site header or footer", e);
+      logger.atWarning().withCause(e).log("Cannot load site header or footer");
     }
   }
 
@@ -60,7 +59,7 @@
         t.load();
         template = t;
       } catch (IOException e) {
-        log.warn("Cannot refresh site header or footer", e);
+        logger.atWarning().withCause(e).log("Cannot refresh site header or footer");
         t = template;
       }
     }
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index f293b2d..5074350 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -43,7 +43,7 @@
         "//lib/antlr:java_runtime",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 3070951..18563ab 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -22,17 +22,18 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gwtorm.server.OrmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Specific version of a secondary index schema. */
 public class Schema<T> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static class Builder<T> {
     private final List<FieldDef<T, ?>> fields = new ArrayList<>();
 
@@ -58,8 +59,6 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(Schema.class);
-
   public static class Values<T> {
     private final FieldDef<T, ?> field;
     private final Iterable<?> values;
@@ -184,7 +183,8 @@
                 try {
                   v = f.get(obj);
                 } catch (OrmException e) {
-                  log.error(String.format("error getting field %s of %s", f.getName(), obj), e);
+                  logger.atSevere().withCause(e).log(
+                      "error getting field %s of %s", f.getName(), obj);
                   return null;
                 }
                 if (v == null) {
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index 4ad0827..24b7a69 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import java.io.OutputStream;
@@ -27,11 +28,9 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.util.io.NullOutputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public abstract class SiteIndexer<K, V, I extends Index<K, V>> {
-  private static final Logger log = LoggerFactory.getLogger(SiteIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Result {
     private final long elapsedNanos;
@@ -128,7 +127,7 @@
     }
 
     private void fail(Throwable t) {
-      log.error("Failed to index " + desc, t);
+      logger.atSevere().withCause(t).log("Failed to index %s", desc);
       ok.set(false);
     }
 
diff --git a/java/com/google/gerrit/lifecycle/BUILD b/java/com/google/gerrit/lifecycle/BUILD
index 191305b..7ba6123 100644
--- a/java/com/google/gerrit/lifecycle/BUILD
+++ b/java/com/google/gerrit/lifecycle/BUILD
@@ -5,7 +5,7 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//lib:guava",
+        "//lib/flogger:api",
         "//lib/guice",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/lifecycle/LifecycleManager.java b/java/com/google/gerrit/lifecycle/LifecycleManager.java
index bbffd49..ba3d7b2 100644
--- a/java/com/google/gerrit/lifecycle/LifecycleManager.java
+++ b/java/com/google/gerrit/lifecycle/LifecycleManager.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.inject.Binding;
@@ -24,10 +25,11 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.util.Providers;
 import java.util.List;
-import org.slf4j.LoggerFactory;
 
 /** Tracks and executes registered {@link LifecycleListener}s. */
 public class LifecycleManager {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final List<Provider<LifecycleListener>> listeners = newList();
   private final List<RegistrationHandle> handles = newList();
 
@@ -105,7 +107,7 @@
       try {
         obj.stop();
       } catch (Throwable err) {
-        LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
+        logger.atWarning().withCause(err).log("Failed to stop %s", obj.getClass());
       }
       startedIndex = i - 1;
     }
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 5505606..3871ced 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.AbstractFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -80,12 +81,10 @@
 import org.apache.lucene.search.TopFieldDocs;
 import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.lucene.store.Directory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Basic Lucene index implementation. */
 public abstract class AbstractLuceneIndex<K, V> implements Index<K, V> {
-  private static final Logger log = LoggerFactory.getLogger(AbstractLuceneIndex.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static String sortFieldName(FieldDef<?, ?> f) {
     return f.getName() + "_SORT";
@@ -145,18 +144,16 @@
                     autoCommitWriter.commit();
                   }
                 } catch (IOException e) {
-                  log.error("Error committing " + index + " Lucene index", e);
+                  logger.atSevere().withCause(e).log("Error committing %s Lucene index", index);
                 } catch (OutOfMemoryError e) {
-                  log.error("Error committing " + index + " Lucene index", e);
+                  logger.atSevere().withCause(e).log("Error committing %s Lucene index", index);
                   try {
                     autoCommitWriter.close();
                   } catch (IOException e2) {
-                    log.error(
-                        "SEVERE: Error closing "
-                            + index
-                            + " Lucene index after OOM;"
+                    logger.atSevere().withCause(e).log(
+                        "SEVERE: Error closing %s Lucene index after OOM;"
                             + " index may be corrupted.",
-                        e);
+                        index);
                   }
                 }
               },
@@ -227,10 +224,11 @@
     writerThread.shutdown();
     try {
       if (!writerThread.awaitTermination(5, TimeUnit.SECONDS)) {
-        log.warn("shutting down " + name + " index with pending Lucene writes");
+        logger.atWarning().log("shutting down %s index with pending Lucene writes", name);
       }
     } catch (InterruptedException e) {
-      log.warn("interrupted waiting for pending Lucene writes of " + name + " index", e);
+      logger.atWarning().withCause(e).log(
+          "interrupted waiting for pending Lucene writes of %s index", name);
     }
     reopenThread.close();
 
@@ -244,7 +242,7 @@
     try {
       searcherManager.maybeRefreshBlocking();
     } catch (IOException e) {
-      log.warn("error finishing pending Lucene writes", e);
+      logger.atWarning().withCause(e).log("error finishing pending Lucene writes");
     }
 
     try {
@@ -252,12 +250,12 @@
     } catch (AlreadyClosedException e) {
       // Ignore.
     } catch (IOException e) {
-      log.warn("error closing Lucene writer", e);
+      logger.atWarning().withCause(e).log("error closing Lucene writer");
     }
     try {
       dir.close();
     } catch (IOException e) {
-      log.warn("error closing Lucene directory", e);
+      logger.atWarning().withCause(e).log("error closing Lucene directory");
     }
   }
 
@@ -450,7 +448,7 @@
       try {
         return reopenThread.waitForGeneration(gen, 0);
       } catch (InterruptedException e) {
-        log.warn("Interrupted waiting for searcher generation", e);
+        logger.atWarning().withCause(e).log("Interrupted waiting for searcher generation");
         return false;
       }
     }
@@ -526,7 +524,7 @@
           try {
             release(searcher);
           } catch (IOException e) {
-            log.warn("cannot release Lucene searcher", e);
+            logger.atWarning().withCause(e).log("cannot release Lucene searcher");
           }
         }
       }
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 0c53215..6cb7751 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -35,10 +35,10 @@
         "//java/com/google/gerrit/server",
         "//lib:guava",
         "//lib:gwtorm",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/lucene:lucene-analyzers-common",
         "//lib/lucene:lucene-core-and-backward-codecs",
         "//lib/lucene:lucene-misc",
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index c8f8fff..7dfaac1 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -34,6 +34,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.index.QueryOptions;
@@ -91,8 +92,6 @@
 import org.apache.lucene.store.RAMDirectory;
 import org.apache.lucene.util.BytesRef;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Secondary index implementation using Apache Lucene.
@@ -102,7 +101,7 @@
  * a committed write and it showing up to other threads' searchers.
  */
 public class LuceneChangeIndex implements ChangeIndex {
-  private static final Logger log = LoggerFactory.getLogger(LuceneChangeIndex.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED);
   static final String ID_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID);
@@ -380,7 +379,7 @@
             try {
               indexes.get(i).release(searchers[i]);
             } catch (IOException e) {
-              log.warn("cannot release Lucene searcher", e);
+              logger.atWarning().withCause(e).log("cannot release Lucene searcher");
             }
           }
         }
diff --git a/java/com/google/gerrit/lucene/LuceneVersionManager.java b/java/com/google/gerrit/lucene/LuceneVersionManager.java
index aabce35..63abea8 100644
--- a/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.lucene;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
@@ -33,12 +34,10 @@
 import java.util.Collection;
 import java.util.TreeMap;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class LuceneVersionManager extends VersionManager {
-  private static final Logger log = LoggerFactory.getLogger(LuceneVersionManager.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static Path getDir(SitePaths sitePaths, String name, Schema<?> schema) {
     return sitePaths.index_dir.resolve(String.format("%s_%04d", name, schema.getVersion()));
@@ -62,7 +61,7 @@
       Path p = getDir(sitePaths, def.getName(), schema);
       boolean isDir = Files.isDirectory(p);
       if (Files.exists(p) && !isDir) {
-        log.warn("Not a directory: {}", p.toAbsolutePath());
+        logger.atWarning().log("Not a directory: %s", p.toAbsolutePath());
       }
       int v = schema.getVersion();
       versions.put(v, new Version<>(schema, v, isDir, cfg.getReady(def.getName(), v)));
@@ -78,7 +77,7 @@
         String versionStr = n.substring(prefix.length());
         Integer v = Ints.tryParse(versionStr);
         if (v == null || versionStr.length() != 4) {
-          log.warn("Unrecognized version in index directory: {}", p.toAbsolutePath());
+          logger.atWarning().log("Unrecognized version in index directory: %s", p.toAbsolutePath());
           continue;
         }
         if (!versions.containsKey(v)) {
@@ -86,7 +85,7 @@
         }
       }
     } catch (IOException e) {
-      log.error("Error scanning index directory: " + sitePaths.index_dir, e);
+      logger.atSevere().withCause(e).log("Error scanning index directory: %s", sitePaths.index_dir);
     }
     return versions;
   }
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index 4500942..6aab7c7 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -141,20 +141,21 @@
         "field not in schema v%s: %s",
         schema.getVersion(),
         p.getField().getName());
-    if (p.getType() == FieldType.INTEGER) {
+    FieldType<?> type = p.getType();
+    if (type == FieldType.INTEGER) {
       return intQuery(p);
-    } else if (p.getType() == FieldType.INTEGER_RANGE) {
+    } else if (type == FieldType.INTEGER_RANGE) {
       return intRangeQuery(p);
-    } else if (p.getType() == FieldType.TIMESTAMP) {
+    } else if (type == FieldType.TIMESTAMP) {
       return timestampQuery(p);
-    } else if (p.getType() == FieldType.EXACT) {
+    } else if (type == FieldType.EXACT) {
       return exactQuery(p);
-    } else if (p.getType() == FieldType.PREFIX) {
+    } else if (type == FieldType.PREFIX) {
       return prefixQuery(p);
-    } else if (p.getType() == FieldType.FULL_TEXT) {
+    } else if (type == FieldType.FULL_TEXT) {
       return fullTextQuery(p);
     } else {
-      throw FieldType.badFieldType(p.getType());
+      throw FieldType.badFieldType(type);
     }
   }
 
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index b32b087..dda2c39 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -8,8 +8,8 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/org/eclipse/jgit:server",
         "//lib:guava",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/metrics/MetricMaker.java b/java/com/google/gerrit/metrics/MetricMaker.java
index 880ba24..401a6d6 100644
--- a/java/com/google/gerrit/metrics/MetricMaker.java
+++ b/java/com/google/gerrit/metrics/MetricMaker.java
@@ -153,4 +153,14 @@
   }
 
   public abstract RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics, Runnable trigger);
+
+  /**
+   * Sanitize the given metric name.
+   *
+   * @param name the name to sanitize.
+   * @return sanitized version of the name.
+   */
+  public String sanitizeMetricName(String name) {
+    return name;
+  }
 }
diff --git a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
index 118ca03..fc53ee7 100644
--- a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
+++ b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -151,8 +151,8 @@
 
   private static void checkCounterDescription(String name, Description desc) {
     checkMetricName(name);
-    checkArgument(!desc.isConstant(), "counters must not be constant");
-    checkArgument(!desc.isGauge(), "counters must not be gauge");
+    checkArgument(!desc.isConstant(), "counter must not be constant");
+    checkArgument(!desc.isGauge(), "counter must not be gauge");
   }
 
   CounterImpl newCounterImpl(String name, boolean isRate) {
@@ -326,7 +326,7 @@
       if (!desc.getAnnotations()
           .get(Description.DESCRIPTION)
           .equals(annotations.get(Description.DESCRIPTION))) {
-        throw new IllegalStateException(String.format("metric %s already defined", name));
+        throw new IllegalStateException(String.format("metric '%s' already defined", name));
       }
     } else {
       descriptions.put(name, desc.getAnnotations());
@@ -339,10 +339,31 @@
   private static void checkMetricName(String name) {
     checkArgument(
         METRIC_NAME_PATTERN.matcher(name).matches(),
-        "metric name must match %s",
+        "invalid metric name '%s': must match pattern '%s'",
+        name,
         METRIC_NAME_PATTERN.pattern());
   }
 
+  @Override
+  public String sanitizeMetricName(String name) {
+    if (METRIC_NAME_PATTERN.matcher(name).matches()) {
+      return name;
+    }
+
+    String first = name.substring(0, 1).replaceFirst("[^\\w-]", "_");
+    if (name.length() == 1) {
+      return first;
+    }
+
+    String result = first + name.substring(1).replaceAll("/[/]+", "/").replaceAll("[^\\w-/]", "_");
+
+    if (result.endsWith("/")) {
+      result = result.substring(0, result.length() - 1);
+    }
+
+    return result;
+  }
+
   static String name(Description.FieldOrdering ordering, String codeName, String fieldValues) {
     if (ordering == FieldOrdering.PREFIX_FIELDS_BASENAME) {
       int s = codeName.lastIndexOf('/');
diff --git a/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
index b028a16..0c69452 100644
--- a/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
+++ b/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -37,11 +37,10 @@
   boolean dataOnly;
 
   @Option(
-    name = "--prefix",
-    aliases = {"-p"},
-    metaVar = "PREFIX",
-    usage = "match metric by exact match or prefix"
-  )
+      name = "--prefix",
+      aliases = {"-p"},
+      metaVar = "PREFIX",
+      usage = "match metric by exact match or prefix")
   List<String> query = new ArrayList<>();
 
   @Inject
diff --git a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java
index bc2846a..10d589a 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanProvider.java
@@ -14,15 +14,14 @@
 
 package com.google.gerrit.metrics.proc;
 
+import com.google.common.flogger.FluentLogger;
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
 import java.lang.reflect.Method;
 import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class OperatingSystemMXBeanProvider {
-  private static final Logger log = LoggerFactory.getLogger(OperatingSystemMXBeanProvider.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final OperatingSystemMXBean sys;
   private final Method getProcessCpuTime;
@@ -41,10 +40,10 @@
             return new OperatingSystemMXBeanProvider(sys);
           }
         } catch (ReflectiveOperationException e) {
-          log.debug("No implementation for {}", name, e);
+          logger.atFine().withCause(e).log("No implementation for %s", name);
         }
       }
-      log.warn("No implementation of UnixOperatingSystemMXBean found");
+      logger.atWarning().log("No implementation of UnixOperatingSystemMXBean found");
       return null;
     }
   }
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 76421fc..95570ec 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -42,8 +42,7 @@
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/sshd",
-        "//java/com/google/gwtexpui/linker:server",
-        "//java/com/google/gwtexpui/server",
+        "//java/com/google/gerrit/util/http",
         "//lib:args4j",
         "//lib:guava",
         "//lib:gwtorm",
@@ -51,11 +50,11 @@
         "//lib:servlet-api-3_1-without-neverlink",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
         "//lib/prolog:cafeteria",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 730f219..c38e7f5 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -20,6 +20,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.client.AuthType;
@@ -66,6 +67,7 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritInstanceNameModule;
 import com.google.gerrit.server.config.GerritOptions;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SysExecutorModule;
 import com.google.gerrit.server.events.EventBroker;
@@ -87,10 +89,8 @@
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
-import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
 import com.google.gerrit.server.restapi.RestApiModule;
-import com.google.gerrit.server.restapi.config.RestCacheAdminModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.InMemoryAccountPatchReviewStore;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
@@ -126,12 +126,10 @@
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Run SSH daemon portions of Gerrit. */
 public class Daemon extends SiteProgram {
-  private static final Logger log = LoggerFactory.getLogger(Daemon.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Option(name = "--enable-httpd", usage = "Enable the internal HTTP daemon")
   private Boolean httpd;
@@ -168,20 +166,18 @@
   private boolean polyGerritDev;
 
   @Option(
-    name = "--init",
-    aliases = {"-i"},
-    usage = "Init site before starting the daemon"
-  )
+      name = "--init",
+      aliases = {"-i"},
+      usage = "Init site before starting the daemon")
   private boolean doInit;
 
   @Option(name = "--stop-only", usage = "Stop the daemon", hidden = true)
   private boolean stopOnly;
 
   @Option(
-    name = "--migrate-to-note-db",
-    usage = "Automatically migrate changes to NoteDb",
-    handler = ExplicitBooleanOptionHandler.class
-  )
+      name = "--migrate-to-note-db",
+      usage = "Automatically migrate changes to NoteDb",
+      handler = ExplicitBooleanOptionHandler.class)
   private boolean migrateToNoteDb;
 
   @Option(name = "--trial", usage = "(With --migrate-to-note-db) " + MigrateToNoteDb.TRIAL_USAGE)
@@ -248,7 +244,7 @@
         new UncaughtExceptionHandler() {
           @Override
           public void uncaughtException(Thread t, Throwable e) {
-            log.error("Thread " + t.getName() + " threw exception", e);
+            logger.atSevere().withCause(e).log("Thread %s threw exception", t.getName());
           }
         });
 
@@ -268,17 +264,17 @@
       start();
       RuntimeShutdown.add(
           () -> {
-            log.info("caught shutdown, cleaning up");
+            logger.atInfo().log("caught shutdown, cleaning up");
             stop();
           });
 
-      log.info("Gerrit Code Review " + myVersion() + " ready");
+      logger.atInfo().log("Gerrit Code Review %s ready", myVersion());
       if (runId != null) {
         try {
           Files.write(runFile, (runId + "\n").getBytes(UTF_8));
           runFile.toFile().setReadable(true, false);
         } catch (IOException err) {
-          log.warn("Cannot write --run-id to " + runFile, err);
+          logger.atWarning().withCause(err).log("Cannot write --run-id to %s", runFile);
         }
       }
 
@@ -298,7 +294,7 @@
       }
       return 0;
     } catch (Throwable err) {
-      log.error("Unable to start daemon", err);
+      logger.atSevere().withCause(err).log("Unable to start daemon");
       return 1;
     }
   }
@@ -365,12 +361,17 @@
       try {
         Files.delete(runFile);
       } catch (IOException err) {
-        log.warn("failed to delete " + runFile, err);
+        logger.atWarning().withCause(err).log("failed to delete %s", runFile);
       }
     }
     manager.stop();
   }
 
+  @Override
+  protected GerritRuntime getGerritRuntime() {
+    return GerritRuntime.DAEMON;
+  }
+
   private boolean sshdOff() {
     return new SshAddressesModule().getListenAddresses(config).isEmpty();
   }
@@ -435,8 +436,6 @@
     }
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new RestApiModule());
-    modules.add(new PluginRestApiModule());
-    modules.add(new RestCacheAdminModule());
     modules.add(new GpgModule(config));
     modules.add(new StartupChecks.Module());
     modules.add(new GerritInstanceNameModule());
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 6e7e3de..b9c7068 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -44,10 +44,9 @@
 /** Initialize a new Gerrit installation. */
 public class Init extends BaseInit {
   @Option(
-    name = "--batch",
-    aliases = {"-b"},
-    usage = "Batch mode; skip interactive prompting"
-  )
+      name = "--batch",
+      aliases = {"-b"},
+      usage = "Batch mode; skip interactive prompting")
   private boolean batchMode;
 
   @Option(name = "--delete-caches", usage = "Delete all persistent caches without asking")
@@ -69,9 +68,8 @@
   private boolean installAllPlugins;
 
   @Option(
-    name = "--secure-store-lib",
-    usage = "Path to jar providing SecureStore implementation class"
-  )
+      name = "--secure-store-lib",
+      usage = "Path to jar providing SecureStore implementation class")
   private String secureStoreLib;
 
   @Option(name = "--dev", usage = "Setup site with default options suitable for developers")
diff --git a/java/com/google/gerrit/pgm/JythonShell.java b/java/com/google/gerrit/pgm/JythonShell.java
index e1a7bd4..88f7b5d 100644
--- a/java/com/google/gerrit/pgm/JythonShell.java
+++ b/java/com/google/gerrit/pgm/JythonShell.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.launcher.GerritLauncher;
 import java.io.File;
 import java.io.IOException;
@@ -24,11 +25,10 @@
 import java.net.URLClassLoader;
 import java.util.ArrayList;
 import java.util.Properties;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class JythonShell {
-  private static final Logger log = LoggerFactory.getLogger(JythonShell.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String STARTUP_RESOURCE = "com/google/gerrit/pgm/Startup.py";
   private static final String STARTUP_FILE = "Startup.py";
 
@@ -79,7 +79,7 @@
 
     try {
       shell = console.getConstructor(new Class<?>[] {}).newInstance();
-      log.info("Jython shell instance created.");
+      logger.atInfo().log("Jython shell instance created.");
     } catch (InstantiationException
         | IllegalAccessException
         | IllegalArgumentException
@@ -170,10 +170,10 @@
       if (in != null) {
         execStream(in, "resource " + p);
       } else {
-        log.error("Cannot load resource " + p);
+        logger.atSevere().log("Cannot load resource %s", p);
       }
     } catch (IOException e) {
-      log.error(e.getMessage(), e);
+      logger.atSevere().withCause(e).log(e.getMessage());
     }
   }
 
@@ -188,15 +188,13 @@
             new Class<?>[] {String.class},
             new Object[] {script.getAbsolutePath()});
       } else {
-        log.info(
-            "User initialization file "
-                + script.getAbsolutePath()
-                + " is not found or not executable");
+        logger.atInfo().log(
+            "User initialization file %s is not found or not executable", script.getAbsolutePath());
       }
     } catch (InvocationTargetException e) {
-      log.error("Exception occurred while loading file " + p + " : ", e);
+      logger.atSevere().withCause(e).log("Exception occurred while loading file %s", p);
     } catch (SecurityException e) {
-      log.error("SecurityException occurred while loading file " + p + " : ", e);
+      logger.atSevere().withCause(e).log("SecurityException occurred while loading file %s", p);
     }
   }
 
@@ -209,7 +207,7 @@
           new Class<?>[] {InputStream.class, String.class},
           new Object[] {in, p});
     } catch (InvocationTargetException e) {
-      log.error("Exception occurred while loading " + p + " : ", e);
+      logger.atSevere().withCause(e).log("Exception occurred while loading %s", p);
     }
   }
 
diff --git a/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java b/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
index 3feab72..4ace62b 100644
--- a/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
+++ b/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
@@ -43,9 +43,8 @@
   private String sourceUrl;
 
   @Option(
-    name = "--chunkSize",
-    usage = "chunk size of fetching from source and push to target on each time"
-  )
+      name = "--chunkSize",
+      usage = "chunk size of fetching from source and push to target on each time")
   private static long chunkSize = 100000;
 
   @Override
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
index 10761c7..0b44ccf 100644
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -55,46 +55,42 @@
   private Integer threads;
 
   @Option(
-    name = "--project",
-    usage =
-        "Only rebuild these projects, do no other migration; incompatible with --change;"
-            + " recommended for debugging only"
-  )
+      name = "--project",
+      usage =
+          "Only rebuild these projects, do no other migration; incompatible with --change;"
+              + " recommended for debugging only")
   private List<String> projects = new ArrayList<>();
 
   @Option(
-    name = "--change",
-    usage =
-        "Only rebuild these changes, do no other migration; incompatible with --project;"
-            + " recommended for debugging only"
-  )
+      name = "--change",
+      usage =
+          "Only rebuild these changes, do no other migration; incompatible with --project;"
+              + " recommended for debugging only")
   private List<Integer> changes = new ArrayList<>();
 
   @Option(
-    name = "--force",
-    usage =
-        "Force rebuilding changes where ReviewDb is still the source of truth, even if they"
-            + " were previously migrated"
-  )
+      name = "--force",
+      usage =
+          "Force rebuilding changes where ReviewDb is still the source of truth, even if they"
+              + " were previously migrated")
   private boolean force;
 
   @Option(name = "--trial", usage = TRIAL_USAGE)
   private boolean trial;
 
   @Option(
-    name = "--sequence-gap",
-    usage =
-        "gap in change sequence numbers between last ReviewDb number and first NoteDb number;"
-            + " negative indicates using the value of noteDb.changes.initialSequenceGap (default"
-            + " 1000)"
-  )
+      name = "--sequence-gap",
+      usage =
+          "gap in change sequence numbers between last ReviewDb number and first NoteDb number;"
+              + " negative indicates using the value of noteDb.changes.initialSequenceGap (default"
+              + " 1000)")
   private int sequenceGap;
 
   @Option(
-    name = "--reindex",
-    usage = "Reindex all changes after migration; defaults to false in trial mode, true otherwise",
-    handler = ExplicitBooleanOptionHandler.class
-  )
+      name = "--reindex",
+      usage =
+          "Reindex all changes after migration; defaults to false in trial mode, true otherwise",
+      handler = ExplicitBooleanOptionHandler.class)
   private Boolean reindex;
 
   private Injector dbInjector;
diff --git a/java/com/google/gerrit/pgm/Passwd.java b/java/com/google/gerrit/pgm/Passwd.java
index e4b362c..f63d2f4 100644
--- a/java/com/google/gerrit/pgm/Passwd.java
+++ b/java/com/google/gerrit/pgm/Passwd.java
@@ -39,11 +39,10 @@
   private String key;
 
   @Argument(
-    metaVar = "SECTION.KEY",
-    index = 0,
-    required = true,
-    usage = "Section and key separated by a dot of the password to set"
-  )
+      metaVar = "SECTION.KEY",
+      index = 0,
+      required = true,
+      usage = "Section and key separated by a dot of the password to set")
   private String sectionAndKey;
 
   @Argument(metaVar = "PASSWORD", index = 1, required = false, usage = "Password to set")
diff --git a/java/com/google/gerrit/pgm/ProtoGen.java b/java/com/google/gerrit/pgm/ProtoGen.java
index 61a0bd5..a882412 100644
--- a/java/com/google/gerrit/pgm/ProtoGen.java
+++ b/java/com/google/gerrit/pgm/ProtoGen.java
@@ -32,12 +32,11 @@
 
 public class ProtoGen extends AbstractProgram {
   @Option(
-    name = "--output",
-    aliases = {"-o"},
-    required = true,
-    metaVar = "FILE",
-    usage = "File to write .proto into"
-  )
+      name = "--output",
+      aliases = {"-o"},
+      required = true,
+      metaVar = "FILE",
+      usage = "File to write .proto into")
   private File file;
 
   @Override
diff --git a/java/com/google/gerrit/pgm/ProtobufImport.java b/java/com/google/gerrit/pgm/ProtobufImport.java
index d970856..0732b28 100644
--- a/java/com/google/gerrit/pgm/ProtobufImport.java
+++ b/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -65,12 +65,11 @@
  */
 public class ProtobufImport extends SiteProgram {
   @Option(
-    name = "--file",
-    aliases = {"-f"},
-    required = true,
-    metaVar = "FILE",
-    usage = "File to import from"
-  )
+      name = "--file",
+      aliases = {"-f"},
+      required = true,
+      metaVar = "FILE",
+      usage = "File to import from")
   private File file;
 
   private final LifecycleManager manager = new LifecycleManager();
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 7bf85dc..2a34efa 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -57,9 +57,8 @@
   private int threads = Runtime.getRuntime().availableProcessors();
 
   @Option(
-    name = "--changes-schema-version",
-    usage = "Schema version to reindex, for changes; default is most recent version"
-  )
+      name = "--changes-schema-version",
+      usage = "Schema version to reindex, for changes; default is most recent version")
   private Integer changesVersion;
 
   @Option(name = "--verbose", usage = "Output debug information for each change")
diff --git a/java/com/google/gerrit/pgm/Rulec.java b/java/com/google/gerrit/pgm/Rulec.java
index 1e54217..add06ef 100644
--- a/java/com/google/gerrit/pgm/Rulec.java
+++ b/java/com/google/gerrit/pgm/Rulec.java
@@ -44,11 +44,10 @@
   private boolean quiet;
 
   @Argument(
-    index = 0,
-    multiValued = true,
-    metaVar = "PROJECT",
-    usage = "project to compile rules for"
-  )
+      index = 0,
+      multiValued = true,
+      metaVar = "PROJECT",
+      usage = "project to compile rules for")
   private List<String> projectNames = new ArrayList<>();
 
   private Injector dbInjector;
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 58876ce..3f10130 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.common.SiteLibraryLoaderUtil;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -40,10 +41,10 @@
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SwitchSecureStore extends SiteProgram {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static String getSecureStoreClassFromGerritConfig(SitePaths sitePaths) {
     FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
     try {
@@ -54,13 +55,10 @@
     return cfg.getString("gerrit", null, "secureStoreClass");
   }
 
-  private static final Logger log = LoggerFactory.getLogger(SwitchSecureStore.class);
-
   @Option(
-    name = "--new-secure-store-lib",
-    usage = "Path to new SecureStore implementation",
-    required = true
-  )
+      name = "--new-secure-store-lib",
+      usage = "Path to new SecureStore implementation",
+      required = true)
   private String newSecureStoreLib;
 
   @Override
@@ -68,7 +66,7 @@
     SitePaths sitePaths = new SitePaths(getSitePath());
     Path newSecureStorePath = Paths.get(newSecureStoreLib);
     if (!Files.exists(newSecureStorePath)) {
-      log.error(String.format("File %s doesn't exist", newSecureStorePath.toAbsolutePath()));
+      logger.atSevere().log("File %s doesn't exist", newSecureStorePath.toAbsolutePath());
       return -1;
     }
 
@@ -76,7 +74,7 @@
     String currentSecureStoreName = getCurrentSecureStoreClassName(sitePaths);
 
     if (currentSecureStoreName.equals(newSecureStore)) {
-      log.error(
+      logger.atSevere().log(
           "Old and new SecureStore implementation names "
               + "are the same. Migration will not work");
       return -1;
@@ -85,10 +83,9 @@
     IoUtil.loadJARs(newSecureStorePath);
     SiteLibraryLoaderUtil.loadSiteLib(sitePaths.lib_dir);
 
-    log.info(
-        "Current secureStoreClass property ({}) will be replaced with {}",
-        currentSecureStoreName,
-        newSecureStore);
+    logger.atInfo().log(
+        "Current secureStoreClass property (%s) will be replaced with %s",
+        currentSecureStoreName, newSecureStore);
     Injector dbInjector = createDbInjector(SINGLE_USER);
     SecureStore currentStore = getSecureStore(currentSecureStoreName, dbInjector);
     SecureStore newStore = getSecureStore(newSecureStore, dbInjector);
@@ -104,7 +101,7 @@
   }
 
   private void migrateProperties(SecureStore currentStore, SecureStore newStore) {
-    log.info("Migrate entries");
+    logger.atInfo().log("Migrate entries");
     for (EntryKey key : currentStore.list()) {
       String[] value = currentStore.getList(key.section, key.subsection, key.name);
       if (value != null) {
@@ -123,26 +120,29 @@
   private void removeOldLib(SitePaths sitePaths, String currentSecureStoreName) throws IOException {
     Path oldSecureStore = findJarWithSecureStore(sitePaths, currentSecureStoreName);
     if (oldSecureStore != null) {
-      log.info("Removing old SecureStore ({}) from lib/ directory", oldSecureStore.getFileName());
+      logger.atInfo().log(
+          "Removing old SecureStore (%s) from lib/ directory", oldSecureStore.getFileName());
       try {
         Files.delete(oldSecureStore);
       } catch (IOException e) {
-        log.error("Cannot remove {}", oldSecureStore.toAbsolutePath(), e);
+        logger.atSevere().withCause(e).log("Cannot remove %s", oldSecureStore.toAbsolutePath());
       }
     } else {
-      log.info(
-          "Cannot find jar with old SecureStore ({}) in lib/ directory", currentSecureStoreName);
+      logger.atInfo().log(
+          "Cannot find jar with old SecureStore (%s) in lib/ directory", currentSecureStoreName);
     }
   }
 
   private void copyNewLib(SitePaths sitePaths, Path newSecureStorePath) throws IOException {
-    log.info("Copy new SecureStore ({}) into lib/ directory", newSecureStorePath.getFileName());
+    logger.atInfo().log(
+        "Copy new SecureStore (%s) into lib/ directory", newSecureStorePath.getFileName());
     Files.copy(newSecureStorePath, sitePaths.lib_dir.resolve(newSecureStorePath.getFileName()));
   }
 
   private void updateGerritConfig(SitePaths sitePaths, String newSecureStore)
       throws IOException, ConfigInvalidException {
-    log.info("Set gerrit.secureStoreClass property of gerrit.config to {}", newSecureStore);
+    logger.atInfo().log(
+        "Set gerrit.secureStoreClass property of gerrit.config to %s", newSecureStore);
     FileBasedConfig config = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
     config.load();
     config.setString("gerrit", null, "secureStoreClass", newSecureStore);
@@ -198,7 +198,7 @@
           return jar;
         }
       } catch (IOException e) {
-        log.error(e.getMessage(), e);
+        logger.atSevere().withCause(e).log(e.getMessage());
       }
     }
     return null;
diff --git a/java/com/google/gerrit/pgm/WarDistribution.java b/java/com/google/gerrit/pgm/WarDistribution.java
index 37ce995..257fb4e 100644
--- a/java/com/google/gerrit/pgm/WarDistribution.java
+++ b/java/com/google/gerrit/pgm/WarDistribution.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.pgm.init.InitPlugins.JAR;
 import static com.google.gerrit.pgm.init.InitPlugins.PLUGIN_DIR;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import com.google.inject.Singleton;
@@ -28,12 +29,10 @@
 import java.util.List;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class WarDistribution implements PluginsDistribution {
-  private static final Logger log = LoggerFactory.getLogger(WarDistribution.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   public void foreach(Processor processor) throws IOException {
@@ -53,8 +52,7 @@
             try (InputStream in = zf.getInputStream(ze)) {
               processor.process(pluginName, in);
             } catch (IOException ioe) {
-              log.error(
-                  String.format("Error opening plugin %s: %s", ze.getName(), ioe.getMessage()));
+              logger.atSevere().log("Error opening plugin %s: %s", ze.getName(), ioe.getMessage());
             }
           }
         }
diff --git a/java/com/google/gerrit/pgm/http/jetty/BUILD b/java/com/google/gerrit/pgm/http/jetty/BUILD
index 86961d6..b1da011 100644
--- a/java/com/google/gerrit/pgm/http/jetty/BUILD
+++ b/java/com/google/gerrit/pgm/http/jetty/BUILD
@@ -10,9 +10,10 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/sshd",
-        "//java/com/google/gwtexpui/server",
+        "//java/com/google/gerrit/util/http",
         "//lib:guava",
         "//lib:servlet-api-3_1",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
@@ -20,7 +21,6 @@
         "//lib/jetty:server",
         "//lib/jetty:servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/log:log4j",
     ],
 )
diff --git a/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java b/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
index 4c2455a..1c43240 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -17,7 +17,8 @@
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 
 import com.google.common.base.Strings;
-import com.google.gwtexpui.server.CacheHeaders;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.util.http.CacheHeaders;
 import java.io.IOException;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
@@ -27,11 +28,9 @@
 import org.eclipse.jetty.server.HttpConnection;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.handler.ErrorHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class HiddenErrorHandler extends ErrorHandler {
-  private static final Logger log = LoggerFactory.getLogger(HiddenErrorHandler.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   public void handle(
@@ -79,7 +78,7 @@
       if (!Strings.isNullOrEmpty(req.getQueryString())) {
         uri += "?" + req.getQueryString();
       }
-      log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+      logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uri);
     }
   }
 }
diff --git a/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index 96cf7be..9354209 100644
--- a/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -61,6 +61,7 @@
  * Jetty's HTTP parser to crash, so we instead block the SSH execution queue thread and ask Jetty to
  * resume processing on the web service thread.
  */
+@SuppressWarnings("deprecation")
 @Singleton
 public class ProjectQoSFilter implements Filter {
   private static final String ATT_SPACE = ProjectQoSFilter.class.getName();
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index 4b53b67..c781a60 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -23,9 +23,9 @@
         "//lib:gwtorm",
         "//lib:h2",
         "//lib/commons:validator",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 88e48aa..deaf139 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -75,12 +76,10 @@
 import java.util.List;
 import java.util.Set;
 import javax.sql.DataSource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Initialize a new Gerrit installation. */
 public class BaseInit extends SiteProgram {
-  private static final Logger log = LoggerFactory.getLogger(BaseInit.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final boolean standalone;
   private final boolean initDb;
@@ -205,7 +204,8 @@
       }
       return names;
     } catch (FileNotFoundException e) {
-      log.warn("Couldn't find distribution archive location. No plugin will be installed");
+      logger.atWarning().log(
+          "Couldn't find distribution archive location. No plugin will be installed");
       return null;
     }
   }
diff --git a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index 5073200..9fd3f16 100644
--- a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init.api;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.SitePaths;
@@ -26,11 +27,9 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RepositoryCache;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class AllProjectsConfig extends VersionedMetaDataOnInit {
-  private static final Logger log = LoggerFactory.getLogger(AllProjectsConfig.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private Config cfg;
   private GroupList groupList;
@@ -64,7 +63,9 @@
     return GroupList.parse(
         new Project.NameKey(project),
         readUTF8(GroupList.FILE_NAME),
-        error -> log.error("Error parsing file {}: {}", GroupList.FILE_NAME, error.getMessage()));
+        error ->
+            logger.atSevere().log(
+                "Error parsing file %s: %s", GroupList.FILE_NAME, error.getMessage()));
   }
 
   public void save(String pluginName, String message) throws IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/pgm/init/api/BUILD b/java/com/google/gerrit/pgm/init/api/BUILD
index d84261c..bc418dd 100644
--- a/java/com/google/gerrit/pgm/init/api/BUILD
+++ b/java/com/google/gerrit/pgm/init/api/BUILD
@@ -9,9 +9,9 @@
         "//java/com/google/gerrit/server",
         "//lib:guava",
         "//lib:gwtorm",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index 91647fb..7fe3bfa 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -21,9 +21,9 @@
         "//lib:guava",
         "//lib:gwtorm",
         "//lib/commons:dbcp",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
     ],
diff --git a/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 8d04be8..413e0fa 100644
--- a/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -17,6 +17,7 @@
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -36,12 +37,10 @@
 import java.util.concurrent.Future;
 import java.util.zip.GZIPOutputStream;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Compresses the old error logs. */
 public class LogFileCompressor implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(LogFileCompressor.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends LifecycleModule {
     @Override
@@ -113,10 +112,10 @@
           }
         }
       } catch (IOException e) {
-        log.error("Error listing logs to compress in " + logs_dir, e);
+        logger.atSevere().withCause(e).log("Error listing logs to compress in %s", logs_dir);
       }
     } catch (Exception e) {
-      log.error("Failed to compress log files: " + e.getMessage(), e);
+      logger.atSevere().withCause(e).log("Failed to compress log files: %s", e.getMessage());
     }
   }
 
@@ -156,11 +155,11 @@
       }
       Files.delete(src);
     } catch (IOException e) {
-      log.error("Cannot compress " + src, e);
+      logger.atSevere().withCause(e).log("Cannot compress %s", src);
       try {
         Files.deleteIfExists(tmp);
       } catch (IOException e2) {
-        log.warn("Failed to delete temporary log file " + tmp, e2);
+        logger.atWarning().withCause(e2).log("Failed to delete temporary log file %s", tmp);
       }
     }
   }
diff --git a/java/com/google/gerrit/pgm/util/RuntimeShutdown.java b/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
index c9df7e7..c5e8567 100644
--- a/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
+++ b/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.common.flogger.FluentLogger;
 import java.util.ArrayList;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class RuntimeShutdown {
   private static final ShutdownCallback cb = new ShutdownCallback();
@@ -45,7 +44,7 @@
   private RuntimeShutdown() {}
 
   private static class ShutdownCallback extends Thread {
-    private static final Logger log = LoggerFactory.getLogger(ShutdownCallback.class);
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
     private final List<Runnable> tasks = new ArrayList<>();
     private boolean shutdownStarted;
@@ -72,7 +71,7 @@
 
     @Override
     public void run() {
-      log.debug("Graceful shutdown requested");
+      logger.atFine().log("Graceful shutdown requested");
 
       List<Runnable> taskList;
       synchronized (this) {
@@ -84,11 +83,11 @@
         try {
           task.run();
         } catch (Exception err) {
-          log.error("Cleanup task failed", err);
+          logger.atSevere().withCause(err).log("Cleanup task failed");
         }
       }
 
-      log.debug("Shutdown complete");
+      logger.atFine().log("Shutdown complete");
 
       synchronized (this) {
         shutdownComplete = true;
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index b59e085..057496f 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
@@ -64,10 +65,9 @@
 
 public abstract class SiteProgram extends AbstractProgram {
   @Option(
-    name = "--site-path",
-    aliases = {"-d"},
-    usage = "Local directory containing site data"
-  )
+      name = "--site-path",
+      aliases = {"-d"},
+      usage = "Local directory containing site data")
   private void setSitePath(String path) {
     sitePath = Paths.get(path);
   }
@@ -155,6 +155,13 @@
         });
     Module configModule = new GerritServerConfigModule();
     modules.add(configModule);
+    modules.add(
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(GerritRuntime.class).toInstance(getGerritRuntime());
+          }
+        });
     Injector cfgInjector = Guice.createInjector(sitePathModule, configModule);
     Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     String dbType;
@@ -219,6 +226,11 @@
     }
   }
 
+  /** Returns the current runtime used by this Gerrit program. */
+  protected GerritRuntime getGerritRuntime() {
+    return GerritRuntime.BATCH;
+  }
+
   protected final String getConfiguredSecureStoreClass() {
     return getSecureStoreClassName(sitePath);
   }
diff --git a/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/java/com/google/gerrit/pgm/util/ThreadLimiter.java
index d609c34..64f703bd 100644
--- a/java/com/google/gerrit/pgm/util/ThreadLimiter.java
+++ b/java/com/google/gerrit/pgm/util/ThreadLimiter.java
@@ -14,19 +14,18 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 // TODO(dborowitz): Not necessary once we switch to NoteDb.
 /** Utility to limit threads used by a batch program. */
 public class ThreadLimiter {
-  private static final Logger log = LoggerFactory.getLogger(ThreadLimiter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static int limitThreads(Injector dbInjector, int threads) {
     return limitThreads(
@@ -41,7 +40,7 @@
     boolean usePool = cfg.getBoolean("database", "connectionpool", dst.usePool());
     int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
     if (usePool && threads > poolLimit) {
-      log.warn("Limiting program to " + poolLimit + " threads due to database.poolLimit");
+      logger.atWarning().log("Limiting program to %d threads due to database.poolLimit", poolLimit);
       return poolLimit;
     }
     return threads;
diff --git a/java/com/google/gerrit/reviewdb/client/Comment.java b/java/com/google/gerrit/reviewdb/client/Comment.java
index 3d19da4..207643e 100644
--- a/java/com/google/gerrit/reviewdb/client/Comment.java
+++ b/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -210,6 +210,8 @@
   public String parentUuid;
   public Range range;
   public String tag;
+
+  // Hex commit SHA1 of the commit of the patchset to which this comment applies.
   public String revId;
   public String serverId;
   public boolean unresolved;
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/ApprovalsUtil.java
index 8ffe33d..8365ddb 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
@@ -66,8 +67,6 @@
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Utility functions to manipulate patchset approvals.
@@ -82,7 +81,7 @@
  */
 @Singleton
 public class ApprovalsUtil {
-  private static final Logger log = LoggerFactory.getLogger(ApprovalsUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final Ordering<PatchSetApproval> SORT_APPROVALS =
       Ordering.from(comparing(PatchSetApproval::getGranted));
@@ -271,11 +270,9 @@
               .database(db)
               .test(ChangePermission.READ);
     } catch (IOException | PermissionBackendException e) {
-      log.warn(
-          String.format(
-              "Failed to check if account %d can see change %d",
-              accountId.get(), notes.getChangeId().get()),
-          e);
+      logger.atWarning().withCause(e).log(
+          "Failed to check if account %d can see change %d",
+          accountId.get(), notes.getChangeId().get());
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index ab2c26b..280a467 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -71,13 +71,13 @@
         "//lib/commons:lang",
         "//lib/commons:net",
         "//lib/commons:validator",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jsoup",
-        "//lib/log:api",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
         "//lib/lucene:lucene-analyzers-common",
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index 56359ce..d90f5d0 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -45,7 +45,7 @@
       Ordering.from(comparingInt(PatchSet::getPatchSetId));
 
   public static String formatChangeUrl(String canonicalWebUrl, Change change) {
-    return canonicalWebUrl + "#/c/" + change.getProject().get() + "/+/" + change.getChangeId();
+    return canonicalWebUrl + "c/" + change.getProject().get() + "/+/" + change.getChangeId();
   }
 
   /** @return a new unique identifier for change message entities. */
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 56b1724..c178137 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
 import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
@@ -62,7 +61,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -130,8 +128,6 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final NotesMigration migration;
-  private final PatchListCache patchListCache;
-  private final PatchSetUtil psUtil;
   private final String serverId;
 
   @Inject
@@ -139,14 +135,10 @@
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
       NotesMigration migration,
-      PatchListCache patchListCache,
-      PatchSetUtil psUtil,
       @GerritServerId String serverId) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.migration = migration;
-    this.patchListCache = patchListCache;
-    this.psUtil = psUtil;
     this.serverId = serverId;
   }
 
@@ -374,31 +366,6 @@
     return sort(comments);
   }
 
-  @Deprecated // To be used only by HasDraftByLegacyPredicate.
-  public List<Change.Id> changesWithDraftsByAuthor(ReviewDb db, Account.Id author)
-      throws OrmException {
-    if (!migration.readChanges()) {
-      return FluentIterable.from(db.patchComments().draftByAuthor(author))
-          .transform(plc -> plc.getPatchSetId().getParentKey())
-          .toList();
-    }
-
-    List<Change.Id> changes = new ArrayList<>();
-    try (Repository repo = repoManager.openRepository(allUsers)) {
-      for (String refName : repo.getRefDatabase().getRefs(RefNames.REFS_DRAFT_COMMENTS).keySet()) {
-        Account.Id accountId = Account.Id.fromRefSuffix(refName);
-        Change.Id changeId = Change.Id.fromRefPart(refName);
-        if (accountId == null || changeId == null) {
-          continue;
-        }
-        changes.add(changeId);
-      }
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    return changes;
-  }
-
   public void putComments(
       ReviewDb db, ChangeUpdate update, PatchLineComment.Status status, Iterable<Comment> comments)
       throws OrmException {
@@ -549,39 +516,4 @@
     return COMMENT_ORDER.sortedCopy(
         FluentIterable.from(comments).transform(plc -> plc.asComment(serverId)));
   }
-
-  public void publish(
-      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag)
-      throws OrmException {
-    ChangeNotes notes = ctx.getNotes();
-    checkArgument(notes != null);
-    if (drafts.isEmpty()) {
-      return;
-    }
-
-    Map<PatchSet.Id, PatchSet> patchSets =
-        psUtil.getAsMap(
-            ctx.getDb(), notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet()));
-    for (Comment d : drafts) {
-      PatchSet ps = patchSets.get(psId(notes, d));
-      if (ps == null) {
-        throw new OrmException("patch set " + ps + " not found");
-      }
-      d.writtenOn = ctx.getWhen();
-      d.tag = tag;
-      // Draft may have been created by a different real user; copy the current real user. (Only
-      // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
-      ctx.getUser().updateRealAccountId(d::setRealAuthor);
-      try {
-        setCommentRevId(d, patchListCache, notes.getChange(), ps);
-      } catch (PatchListNotAvailableException e) {
-        throw new OrmException(e);
-      }
-    }
-    putComments(ctx.getDb(), ctx.getUpdate(psId), PUBLISHED, drafts);
-  }
-
-  private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
-    return new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
-  }
 }
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index 40800b4..b7bc036 100644
--- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
+++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -18,6 +18,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
@@ -36,8 +37,6 @@
 import java.util.HashSet;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * With groups in NoteDb, the capability of creating a group is expressed as a {@code CREATE}
@@ -52,7 +51,7 @@
  */
 @Singleton
 public class CreateGroupPermissionSyncer implements ChangeMergedListener {
-  private static final Logger log = LoggerFactory.getLogger(CreateGroupPermissionSyncer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AllProjectsName allProjects;
   private final AllUsersName allUsers;
@@ -135,7 +134,7 @@
     try {
       syncIfNeeded();
     } catch (IOException | ConfigInvalidException e) {
-      log.error("Can't sync create group permissions", e);
+      logger.atSevere().withCause(e).log("Can't sync create group permissions");
     }
   }
 }
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index eb3a3fd..03b9f54 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -157,4 +157,17 @@
   public Optional<ExternalId.Key> getLastLoginExternalIdKey() {
     return get(lastLoginExternalIdPropertyKey);
   }
+
+  /**
+   * Checks if the current user has the same account id of another.
+   *
+   * <p>Provide a generic interface for allowing subclasses to define whether two accounts represent
+   * the same account id.
+   *
+   * @param other user to compare
+   * @return true if the two users have the same account id
+   */
+  public boolean hasSameAccountId(CurrentUser other) {
+    return false;
+  }
 }
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 023e8e3..16546f9 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -523,6 +523,11 @@
         realUser);
   }
 
+  @Override
+  public boolean hasSameAccountId(CurrentUser other) {
+    return getAccountId().get() == other.getAccountId().get();
+  }
+
   private String guessHost() {
     String host = null;
     SocketAddress remotePeer = null;
diff --git a/java/com/google/gerrit/server/LibModuleLoader.java b/java/com/google/gerrit/server/LibModuleLoader.java
index 4ec7d2d..d1067e1 100644
--- a/java/com/google/gerrit/server/LibModuleLoader.java
+++ b/java/com/google/gerrit/server/LibModuleLoader.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -24,12 +25,10 @@
 import java.util.Arrays;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Loads configured Guice modules from {@code gerrit.installModule}. */
 public class LibModuleLoader {
-  private static final Logger log = LoggerFactory.getLogger(LibModuleLoader.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static List<Module> loadModules(Injector parent) {
     Config cfg = getConfig(parent);
@@ -44,7 +43,7 @@
 
   private static Module createModule(Injector injector, String className) {
     Module m = injector.getInstance(loadModule(className));
-    log.info("Installed module {}", className);
+    logger.atInfo().log("Installed module %s", className);
     return m;
   }
 
@@ -54,7 +53,7 @@
       return (Class<Module>) Class.forName(className);
     } catch (ClassNotFoundException | LinkageError e) {
       String msg = "Cannot load LibModule " + className;
-      log.error(msg, e);
+      logger.atSevere().withCause(e).log(msg);
       throw new ProvisionException(msg, e);
     }
   }
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
new file mode 100644
index 0000000..a90f3e7
--- /dev/null
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -0,0 +1,83 @@
+// 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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+import java.util.Map;
+
+@Singleton
+public class PublishCommentUtil {
+  private final PatchListCache patchListCache;
+  private final PatchSetUtil psUtil;
+  private final CommentsUtil commentsUtil;
+
+  @Inject
+  PublishCommentUtil(
+      CommentsUtil commentsUtil, PatchListCache patchListCache, PatchSetUtil psUtil) {
+    this.commentsUtil = commentsUtil;
+    this.psUtil = psUtil;
+    this.patchListCache = patchListCache;
+  }
+
+  public void publish(
+      ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag)
+      throws OrmException {
+    ChangeNotes notes = ctx.getNotes();
+    checkArgument(notes != null);
+    if (drafts.isEmpty()) {
+      return;
+    }
+
+    Map<Id, PatchSet> patchSets =
+        psUtil.getAsMap(
+            ctx.getDb(), notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet()));
+    for (Comment d : drafts) {
+      PatchSet ps = patchSets.get(psId(notes, d));
+      if (ps == null) {
+        throw new OrmException("patch set " + ps + " not found");
+      }
+      d.writtenOn = ctx.getWhen();
+      d.tag = tag;
+      // Draft may have been created by a different real user; copy the current real user. (Only
+      // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
+      ctx.getUser().updateRealAccountId(d::setRealAuthor);
+      try {
+        CommentsUtil.setCommentRevId(d, patchListCache, notes.getChange(), ps);
+      } catch (PatchListNotAvailableException e) {
+        throw new OrmException(e);
+      }
+    }
+    commentsUtil.putComments(ctx.getDb(), ctx.getUpdate(psId), PUBLISHED, drafts);
+  }
+
+  private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
+    return new PatchSet.Id(notes.getChangeId(), c.key.patchSetId);
+  }
+}
diff --git a/java/com/google/gerrit/server/RequestCleanup.java b/java/com/google/gerrit/server/RequestCleanup.java
index ea60682..7ed9287 100644
--- a/java/com/google/gerrit/server/RequestCleanup.java
+++ b/java/com/google/gerrit/server/RequestCleanup.java
@@ -14,17 +14,16 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.inject.servlet.RequestScoped;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Registers cleanup activities to be completed when a scope ends. */
 @RequestScoped
 public class RequestCleanup implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(RequestCleanup.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final List<Runnable> cleanup = new LinkedList<>();
   private boolean ran;
@@ -47,7 +46,7 @@
         try {
           i.next().run();
         } catch (Throwable err) {
-          log.error("Failed to execute per-request cleanup", err);
+          logger.atSevere().withCause(err).log("Failed to execute per-request cleanup");
         }
         i.remove();
       }
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index e2c08ce..0fbf200 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
@@ -70,11 +71,11 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class StarredChangesUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   @AutoValue
   public abstract static class StarField {
     private static final String SEPARATOR = ":";
@@ -154,8 +155,6 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(StarredChangesUtil.class);
-
   public static final String DEFAULT_LABEL = "star";
   public static final String IGNORE_LABEL = "ignore";
   public static final String REVIEWED_LABEL = "reviewed";
@@ -305,11 +304,9 @@
       Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId));
       return ref != null ? ref.getObjectId() : ObjectId.zeroId();
     } catch (IOException e) {
-      log.error(
-          String.format(
-              "Getting star object ID for account %d on change %d failed",
-              accountId.get(), changeId.get()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Getting star object ID for account %d on change %d failed",
+          accountId.get(), changeId.get());
       return ObjectId.zeroId();
     }
   }
@@ -479,6 +476,11 @@
 
   private void deleteRef(Repository repo, String refName, ObjectId oldObjectId)
       throws IOException, OrmException {
+    if (ObjectId.zeroId().equals(oldObjectId)) {
+      // ref doesn't exist
+      return;
+    }
+
     RefUpdate u = repo.updateRef(refName);
     u.setForceUpdate(true);
     u.setExpectedOldObjectId(oldObjectId);
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index afa4609..39a2328 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
@@ -37,19 +38,17 @@
 import com.google.inject.Singleton;
 import java.util.Collections;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class WebLinks {
-  private static final Logger log = LoggerFactory.getLogger(WebLinks.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final Predicate<WebLinkInfo> INVALID_WEBLINK =
       link -> {
         if (link == null) {
           return false;
         } else if (Strings.isNullOrEmpty(link.name) || Strings.isNullOrEmpty(link.url)) {
-          log.warn(String.format("%s is missing name and/or url", link.getClass().getName()));
+          logger.atWarning().log("%s is missing name and/or url", link.getClass().getName());
           return false;
         }
         return true;
@@ -60,7 +59,7 @@
         if (link == null) {
           return false;
         } else if (Strings.isNullOrEmpty(link.name) || Strings.isNullOrEmpty(link.url)) {
-          log.warn(String.format("%s is missing name and/or url", link.getClass().getName()));
+          logger.atWarning().log("%s is missing name and/or url", link.getClass().getName());
           return false;
         }
         return true;
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 0648f9f..76bfcfd 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -19,6 +19,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
@@ -44,13 +45,11 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Caches important (but small) account state to avoid database hits. */
 @Singleton
 public class AccountCacheImpl implements AccountCache {
-  private static final Logger log = LoggerFactory.getLogger(AccountCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String BYID_NAME = "accounts";
 
@@ -89,7 +88,7 @@
     try {
       return byId.get(accountId).orElse(missing(accountId));
     } catch (ExecutionException e) {
-      log.warn("Cannot load AccountState for " + accountId, e);
+      logger.atWarning().withCause(e).log("Cannot load AccountState for %s", accountId);
       return missing(accountId);
     }
   }
@@ -99,7 +98,7 @@
     try {
       return byId.get(accountId);
     } catch (ExecutionException e) {
-      log.warn("Cannot load AccountState for ID " + accountId, e);
+      logger.atWarning().withCause(e).log("Cannot load AccountState for ID %s", accountId);
       return null;
     }
   }
@@ -126,14 +125,14 @@
     try {
       futures = executor.invokeAll(callables);
     } catch (InterruptedException e) {
-      log.error("Cannot load AccountStates", e);
+      logger.atSevere().withCause(e).log("Cannot load AccountStates");
       return ImmutableMap.of();
     }
     for (Future<Optional<AccountState>> f : futures) {
       try {
         f.get().ifPresent(s -> accountStates.put(s.getAccount().getId(), s));
       } catch (InterruptedException | ExecutionException e) {
-        log.error("Cannot load AccountState", e);
+        logger.atSevere().withCause(e).log("Cannot load AccountState");
       }
     }
     return accountStates;
@@ -147,7 +146,7 @@
           .map(e -> get(e.accountId()))
           .orElseGet(Optional::empty);
     } catch (IOException | ConfigInvalidException e) {
-      log.warn("Cannot load AccountState for username " + username, e);
+      logger.atWarning().withCause(e).log("Cannot load AccountState for username %s", username);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index 73c47ad..b0dc527 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -27,12 +28,10 @@
 import com.google.inject.Provider;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Runnable to enable scheduling account deactivations to run periodically */
 public class AccountDeactivator implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(AccountDeactivator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends LifecycleModule {
     @Override
@@ -84,7 +83,7 @@
 
   @Override
   public void run() {
-    log.info("Running account deactivations");
+    logger.atInfo().log("Running account deactivations");
     try {
       int numberOfAccountsDeactivated = 0;
       for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) {
@@ -92,10 +91,11 @@
           numberOfAccountsDeactivated++;
         }
       }
-      log.info(
-          "Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated);
+      logger.atInfo().log(
+          "Deactivations complete, %d account(s) were deactivated", numberOfAccountsDeactivated);
     } catch (Exception e) {
-      log.error("Failed to complete deactivation of accounts: " + e.getMessage(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to complete deactivation of accounts: %s", e.getMessage());
     }
   }
 
@@ -105,22 +105,19 @@
     }
 
     String userName = accountState.getUserName().get();
-    log.debug("processing account " + userName);
+    logger.atFine().log("processing account %s", userName);
     try {
       if (realm.accountBelongsToRealm(accountState.getExternalIds()) && !realm.isActive(userName)) {
         sif.deactivate(accountState.getAccount().getId());
-        log.info("deactivated account " + userName);
+        logger.atInfo().log("deactivated account %s", userName);
         return true;
       }
     } catch (ResourceConflictException e) {
-      log.info("Account {} already deactivated, continuing...", userName);
+      logger.atInfo().log("Account %s already deactivated, continuing...", userName);
     } catch (Exception e) {
-      log.error(
-          "Error deactivating account: {} ({}) {}",
-          userName,
-          accountState.getAccount().getId(),
-          e.getMessage(),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Error deactivating account: %s (%s) %s",
+          userName, accountState.getAccount().getId(), e.getMessage());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 009623a..e2194cc 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -56,13 +57,11 @@
 import java.util.function.Consumer;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Tracks authentication related details for user accounts. */
 @Singleton
 public class AccountManager {
-  private static final Logger log = LoggerFactory.getLogger(AccountManager.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Sequences sequences;
   private final Accounts accounts;
@@ -144,24 +143,23 @@
             // An inconsistency is detected in the database, having a record for scheme "username:"
             // but no record for scheme "gerrit:". Try to recover by linking
             // "gerrit:" identity to the existing account.
-            log.warn(
-                "User {} already has an account; link new identity to the existing account.",
+            logger.atWarning().log(
+                "User %s already has an account; link new identity to the existing account.",
                 who.getUserName());
             return link(existingId.get().accountId(), who);
           }
         }
         // New account, automatically create and return.
-        log.debug("External ID not found. Attempting to create new account.");
+        logger.atFine().log("External ID not found. Attempting to create new account.");
         return create(who);
       }
 
       ExternalId extId = optionalExtId.get();
       Optional<AccountState> accountState = byIdCache.get(extId.accountId());
       if (!accountState.isPresent()) {
-        log.error(
-            "Authentication with external ID {} failed. Account {} doesn't exist.",
-            extId.key().get(),
-            extId.accountId().get());
+        logger.atSevere().log(
+            "Authentication with external ID %s failed. Account %s doesn't exist.",
+            extId.key().get(), extId.accountId().get());
         throw new AccountException("Authentication error, account not found");
       }
 
@@ -195,12 +193,11 @@
       }
       setInactiveFlag.deactivate(extId.get().accountId());
     } catch (Exception e) {
-      log.error(
-          "Unable to deactivate account "
-              + authRequest
-                  .getUserName()
-                  .orElse(" for external ID key " + authRequest.getExternalIdKey().get()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Unable to deactivate account %s",
+          authRequest
+              .getUserName()
+              .orElse(" for external ID key " + authRequest.getExternalIdKey().get()));
     }
   }
 
@@ -261,12 +258,11 @@
         && who.getUserName().isPresent()
         && !who.getUserName().equals(user.getUserName())) {
       if (user.getUserName().isPresent()) {
-        log.warn(
-            "Not changing already set username {} to {}",
-            user.getUserName().get(),
-            who.getUserName().get());
+        logger.atWarning().log(
+            "Not changing already set username %s to %s",
+            user.getUserName().get(), who.getUserName().get());
       } else {
-        log.warn("Not setting username to {}", who.getUserName().get());
+        logger.atWarning().log("Not setting username to %s", who.getUserName().get());
       }
     }
 
@@ -285,11 +281,11 @@
   private AuthResult create(AuthRequest who)
       throws OrmException, AccountException, IOException, ConfigInvalidException {
     Account.Id newId = new Account.Id(sequences.nextAccountId());
-    log.debug("Assigning new Id {} to account", newId);
+    logger.atFine().log("Assigning new Id %s to account", newId);
 
     ExternalId extId =
         ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
-    log.debug("Created external Id: {}", extId);
+    logger.atFine().log("Created external Id: %s", extId);
     checkEmailNotUsed(extId);
     ExternalId userNameExtId =
         who.getUserName().isPresent() ? createUsername(newId, who.getUserName().get()) : null;
@@ -376,9 +372,9 @@
       return;
     }
 
-    log.warn(
-        "Email {} is already assigned to account {};"
-            + " cannot create external ID {} with the same email for account {}.",
+    logger.atWarning().log(
+        "Email %s is already assigned to account %s;"
+            + " cannot create external ID %s with the same email for account %s.",
         email,
         existingExtIdsWithEmail.iterator().next().accountId().get(),
         extIdToBeCreated.key().get(),
@@ -414,7 +410,7 @@
   public AuthResult link(Account.Id to, AuthRequest who)
       throws AccountException, OrmException, IOException, ConfigInvalidException {
     Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
-    log.debug("Link another authentication identity to an existing account");
+    logger.atFine().log("Link another authentication identity to an existing account");
     if (optionalExtId.isPresent()) {
       ExternalId extId = optionalExtId.get();
       if (!extId.accountId().equals(to)) {
@@ -423,7 +419,7 @@
       }
       update(who, extId);
     } else {
-      log.debug("Linking new external ID to the existing account");
+      logger.atFine().log("Linking new external ID to the existing account");
       ExternalId newExtId =
           ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
       checkEmailNotUsed(newExtId);
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 14a0e92..e56ad72 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -23,6 +23,7 @@
 import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
@@ -41,8 +42,6 @@
 import java.util.Optional;
 import org.apache.commons.codec.DecoderException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Superset of all information related to an Account. This includes external IDs, project watches,
@@ -52,7 +51,7 @@
  * account cache (see {@link AccountCache#get(Account.Id)}).
  */
 public class AccountState {
-  private static final Logger logger = LoggerFactory.getLogger(AccountState.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
       a -> a.getAccount().getId();
@@ -224,8 +223,7 @@
         try {
           return HashedPassword.decode(hashedStr).checkPassword(password);
         } catch (DecoderException e) {
-          logger.error(
-              String.format("DecoderException for user %s: %s ", username, e.getMessage()));
+          logger.atSevere().log("DecoderException for user %s: %s ", username, e.getMessage());
           return false;
         }
       }
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index 62b1d87..7dff74c 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -18,6 +18,7 @@
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -35,13 +36,11 @@
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Class to access accounts. */
 @Singleton
 public class Accounts {
-  private static final Logger log = LoggerFactory.getLogger(Accounts.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
@@ -85,7 +84,7 @@
         try {
           read(repo, accountId).ifPresent(accounts::add);
         } catch (Exception e) {
-          log.error(String.format("Ignoring invalid account %s", accountId.get()), e);
+          logger.atSevere().withCause(e).log("Ignoring invalid account %s", accountId);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index a20aab7..e7aae15 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
@@ -29,13 +30,11 @@
 import com.google.inject.name.Named;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Tracks group objects in memory for efficient access. */
 @Singleton
 public class GroupCacheImpl implements GroupCache {
-  private static final Logger log = LoggerFactory.getLogger(GroupCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String BYID_NAME = "groups";
   private static final String BYNAME_NAME = "groups_byname";
@@ -82,7 +81,7 @@
     try {
       return byId.get(groupId);
     } catch (ExecutionException e) {
-      log.warn("Cannot load group " + groupId, e);
+      logger.atWarning().withCause(e).log("Cannot load group %s", groupId);
       return Optional.empty();
     }
   }
@@ -95,7 +94,7 @@
     try {
       return byName.get(name.get());
     } catch (ExecutionException e) {
-      log.warn(String.format("Cannot look up group %s by name", name.get()), e);
+      logger.atWarning().withCause(e).log("Cannot look up group %s by name", name.get());
       return Optional.empty();
     }
   }
@@ -109,7 +108,7 @@
     try {
       return byUUID.get(groupUuid.get());
     } catch (ExecutionException e) {
-      log.warn(String.format("Cannot look up group %s by uuid", groupUuid.get()), e);
+      logger.atWarning().withCause(e).log("Cannot look up group %s by uuid", groupUuid.get());
       return Optional.empty();
     }
   }
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index ba81c6a..f262a79 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -21,6 +21,7 @@
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.cache.CacheModule;
@@ -37,13 +38,12 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.concurrent.ExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Tracks group inclusions in memory for efficient access. */
 @Singleton
 public class GroupIncludeCacheImpl implements GroupIncludeCache {
-  private static final Logger log = LoggerFactory.getLogger(GroupIncludeCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String PARENT_GROUPS_NAME = "groups_bysubgroup";
   private static final String GROUPS_WITH_MEMBER_NAME = "groups_bymember";
   private static final String EXTERNAL_NAME = "groups_external";
@@ -94,7 +94,7 @@
     try {
       return groupsWithMember.get(memberId);
     } catch (ExecutionException e) {
-      log.warn(String.format("Cannot load groups containing %d as member", memberId.get()));
+      logger.atWarning().withCause(e).log("Cannot load groups containing %s as member", memberId);
       return ImmutableSet.of();
     }
   }
@@ -104,7 +104,7 @@
     try {
       return parentGroups.get(groupId);
     } catch (ExecutionException e) {
-      log.warn("Cannot load included groups", e);
+      logger.atWarning().withCause(e).log("Cannot load included groups");
       return Collections.emptySet();
     }
   }
@@ -132,7 +132,7 @@
     try {
       return external.get(EXTERNAL_NAME);
     } catch (ExecutionException e) {
-      log.warn("Cannot load set of non-internal groups", e);
+      logger.atWarning().withCause(e).log("Cannot load set of non-internal groups");
       return ImmutableList.of();
     }
   }
diff --git a/java/com/google/gerrit/server/account/Preferences.java b/java/com/google/gerrit/server/account/Preferences.java
index 07793bb..aa09675 100644
--- a/java/com/google/gerrit/server/account/Preferences.java
+++ b/java/com/google/gerrit/server/account/Preferences.java
@@ -30,6 +30,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
@@ -54,8 +55,6 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Parses/writes preferences from/to a {@link Config} file.
@@ -86,7 +85,7 @@
  * <p>The preferences are lazily parsed.
  */
 public class Preferences {
-  private static final Logger log = LoggerFactory.getLogger(Preferences.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String PREFERENCES_CONFIG = "preferences.config";
 
@@ -339,7 +338,7 @@
         }
       }
     } catch (IllegalAccessException e) {
-      log.error("Failed to apply default general preferences", e);
+      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
       return GeneralPreferencesInfo.defaults();
     }
     return result;
@@ -358,7 +357,7 @@
         }
       }
     } catch (IllegalAccessException e) {
-      log.error("Failed to apply default diff preferences", e);
+      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
       return DiffPreferencesInfo.defaults();
     }
     return result;
@@ -377,7 +376,7 @@
         }
       }
     } catch (IllegalAccessException e) {
-      log.error("Failed to apply default edit preferences", e);
+      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
       return EditPreferencesInfo.defaults();
     }
     return result;
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index fc9b58a..22e9dbd 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
@@ -39,15 +40,13 @@
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Universal implementation of the GroupBackend that works with the injected set of GroupBackends.
  */
 @Singleton
 public class UniversalGroupBackend implements GroupBackend {
-  private static final Logger log = LoggerFactory.getLogger(UniversalGroupBackend.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<GroupBackend> backends;
 
@@ -80,7 +79,7 @@
     }
     GroupBackend b = backend(uuid);
     if (b == null) {
-      log.debug("Unknown GroupBackend for UUID: " + uuid);
+      logger.atFine().log("Unknown GroupBackend for UUID: %s", uuid);
       return null;
     }
     return b.get(uuid);
@@ -130,7 +129,7 @@
       }
       GroupMembership m = membership(uuid);
       if (m == null) {
-        log.debug("Unknown GroupMembership for UUID: " + uuid);
+        logger.atFine().log("Unknown GroupMembership for UUID: %s", uuid);
         return false;
       }
       return m.contains(uuid);
@@ -146,7 +145,7 @@
         }
         GroupMembership m = membership(uuid);
         if (m == null) {
-          log.debug("Unknown GroupMembership for UUID: " + uuid);
+          logger.atFine().log("Unknown GroupMembership for UUID: %s", uuid);
           continue;
         }
         lookups.put(m, uuid);
@@ -176,7 +175,7 @@
         }
         GroupMembership m = membership(uuid);
         if (m == null) {
-          log.debug("Unknown GroupMembership for UUID: " + uuid);
+          logger.atFine().log("Unknown GroupMembership for UUID: %s", uuid);
           continue;
         }
         lookups.put(m, uuid);
diff --git a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
index 1064546..e2f1bc2 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
@@ -21,12 +22,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.FileMode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** User configured named destinations. */
 public class VersionedAccountDestinations extends VersionedMetaData {
-  private static final Logger log = LoggerFactory.getLogger(VersionedAccountDestinations.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static VersionedAccountDestinations forUser(Account.Id id) {
     return new VersionedAccountDestinations(RefNames.refsUsers(id));
@@ -62,7 +61,8 @@
           destinations.parseLabel(
               label,
               readUTF8(path),
-              error -> log.error("Error parsing file {}: {}", path, error.getMessage()));
+              error ->
+                  logger.atSevere().log("Error parsing file %s: %s", path, error.getMessage()));
         }
       }
     }
diff --git a/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
index b021d24..daf7100 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountQueries.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Named Queries for user accounts. */
 public class VersionedAccountQueries extends VersionedMetaData {
-  private static final Logger log = LoggerFactory.getLogger(VersionedAccountQueries.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static VersionedAccountQueries forUser(Account.Id id) {
     return new VersionedAccountQueries(RefNames.refsUsers(id));
@@ -53,7 +52,8 @@
         QueryList.parse(
             readUTF8(QueryList.FILE_NAME),
             error ->
-                log.error("Error parsing file {}: {}", QueryList.FILE_NAME, error.getMessage()));
+                logger.atSevere().log(
+                    "Error parsing file %s: %s", QueryList.FILE_NAME, error.getMessage()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
new file mode 100644
index 0000000..15f7a0ad4
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -0,0 +1,50 @@
+// 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.account.externalids;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Strings;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import com.google.gerrit.reviewdb.client.Account.Id;
+
+/**
+ * Cache value containing all external IDs.
+ *
+ * <p>All returned fields are unmodifiable.
+ */
+@AutoValue
+public abstract class AllExternalIds {
+  static AllExternalIds create(Multimap<Id, ExternalId> byAccount) {
+    SetMultimap<String, ExternalId> byEmailCopy =
+        MultimapBuilder.hashKeys(byAccount.size()).hashSetValues(1).build();
+    byAccount
+        .values()
+        .stream()
+        .filter(e -> !Strings.isNullOrEmpty(e.email()))
+        .forEach(e -> byEmailCopy.put(e.email(), e));
+
+    return new AutoValue_AllExternalIds(
+        Multimaps.unmodifiableSetMultimap(
+            MultimapBuilder.hashKeys(byAccount.size()).hashSetValues(5).build(byAccount)),
+        byEmailCopy);
+  }
+
+  public abstract SetMultimap<Id, ExternalId> byAccount();
+
+  public abstract SetMultimap<String, ExternalId> byEmail();
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 1f77773..533b1c0 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -14,20 +14,18 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.auto.value.AutoValue;
-import com.google.common.base.Strings;
-import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Multimaps;
 import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Set;
@@ -36,39 +34,23 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Consumer;
 import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Caches external IDs of all accounts. The external IDs are always loaded from NoteDb. */
 @Singleton
 class ExternalIdCacheImpl implements ExternalIdCache {
-  private static final Logger log = LoggerFactory.getLogger(ExternalIdCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static final String CACHE_NAME = "external_ids_map";
 
   private final LoadingCache<ObjectId, AllExternalIds> extIdsByAccount;
   private final ExternalIdReader externalIdReader;
   private final Lock lock;
 
   @Inject
-  ExternalIdCacheImpl(ExternalIdReader externalIdReader) {
-    this.extIdsByAccount =
-        CacheBuilder.newBuilder()
-            // The cached data is potentially pretty large and we are always only interested
-            // in the latest value, hence the maximum cache size is set to 1.
-            // This can lead to extra cache loads in case of the following race:
-            // 1. thread 1 reads the notes ref at revision A
-            // 2. thread 2 updates the notes ref to revision B and stores the derived value
-            //    for B in the cache
-            // 3. thread 1 attempts to read the data for revision A from the cache, and misses
-            // 4. later threads attempt to read at B
-            // In this race unneeded reloads are done in step 3 (reload from revision A) and
-            // step 4 (reload from revision B, because the value for revision B was lost when the
-            // reload from revision A was done, since the cache can hold only one entry).
-            // These reloads could be avoided by increasing the cache size to 2. However the race
-            // window between reading the ref and looking it up in the cache is small so that
-            // it's rare that this race happens. Therefore it's not worth to double the memory
-            // usage of this cache, just to avoid this.
-            .maximumSize(1)
-            .build(new Loader(externalIdReader));
+  ExternalIdCacheImpl(
+      @Named(CACHE_NAME) LoadingCache<ObjectId, AllExternalIds> extIdsByAccount,
+      ExternalIdReader externalIdReader) {
+    this.extIdsByAccount = extIdsByAccount;
     this.externalIdReader = externalIdReader;
     this.lock = new ReentrantLock(true /* fair */);
   }
@@ -154,15 +136,16 @@
       update.accept(m);
       extIdsByAccount.put(newNotesRev, AllExternalIds.create(m));
     } catch (ExecutionException e) {
-      log.warn("Cannot update external IDs", e);
+      logger.atWarning().withCause(e).log("Cannot update external IDs");
     } finally {
       lock.unlock();
     }
   }
 
-  private static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
+  static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
     private final ExternalIdReader externalIdReader;
 
+    @Inject
     Loader(ExternalIdReader externalIdReader) {
       this.externalIdReader = externalIdReader;
     }
@@ -178,31 +161,4 @@
       return AllExternalIds.create(extIdsByAccount);
     }
   }
-
-  /**
-   * Cache value containing all external IDs.
-   *
-   * <p>All returned fields are unmodifiable.
-   */
-  @AutoValue
-  abstract static class AllExternalIds {
-    static AllExternalIds create(Multimap<Account.Id, ExternalId> byAccount) {
-      SetMultimap<String, ExternalId> byEmailCopy =
-          MultimapBuilder.hashKeys(byAccount.size()).hashSetValues(1).build();
-      byAccount
-          .values()
-          .stream()
-          .filter(e -> !Strings.isNullOrEmpty(e.email()))
-          .forEach(e -> byEmailCopy.put(e.email(), e));
-
-      return new AutoValue_ExternalIdCacheImpl_AllExternalIds(
-          Multimaps.unmodifiableSetMultimap(
-              MultimapBuilder.hashKeys(byAccount.size()).hashSetValues(5).build(byAccount)),
-          byEmailCopy);
-    }
-
-    public abstract SetMultimap<Account.Id, ExternalId> byAccount();
-
-    public abstract SetMultimap<String, ExternalId> byEmail();
-  }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
index 8c97144..228b1e6 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -14,11 +14,26 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.inject.AbstractModule;
+import com.google.gerrit.server.account.externalids.ExternalIdCacheImpl.Loader;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.TypeLiteral;
+import java.time.Duration;
+import org.eclipse.jgit.lib.ObjectId;
 
-public class ExternalIdModule extends AbstractModule {
+public class ExternalIdModule extends CacheModule {
   @Override
   protected void configure() {
+    cache(ExternalIdCacheImpl.CACHE_NAME, ObjectId.class, new TypeLiteral<AllExternalIds>() {})
+        // The cached data is potentially pretty large and we are always only interested
+        // in the latest value. However, due to a race condition, it is possible for different
+        // threads to observe different values of the meta ref, and hence request different keys
+        // from the cache. Extend the cache size by 1 to cover this case, but expire the extra
+        // object after a short period of time, since it may be a potentially large amount of
+        // memory.
+        .maximumWeight(2)
+        .expireFromMemoryAfterAccess(Duration.ofMinutes(1))
+        .loader(Loader.class);
+
     bind(ExternalIdCacheImpl.class);
     bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class);
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 1a3d960..8057dd8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
@@ -60,8 +61,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * {@link VersionedMetaData} subclass to update external IDs.
@@ -85,7 +84,7 @@
  * accounts for which external IDs have been updated (see {@link #updateCaches()}).
  */
 public class ExternalIdNotes extends VersionedMetaData {
-  private static final Logger log = LoggerFactory.getLogger(ExternalIdNotes.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final int MAX_NOTE_SZ = 1 << 19;
 
@@ -358,7 +357,8 @@
         try {
           b.add(ExternalId.parse(note.getName(), raw, note.getData()));
         } catch (ConfigInvalidException | RuntimeException e) {
-          log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
+          logger.atSevere().withCause(e).log(
+              "Ignoring invalid external ID note %s", note.getName());
         }
       }
       return b.build();
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index 1d40b53..7872812 100644
--- a/java/com/google/gerrit/server/args4j/ProjectHandler.java
+++ b/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.args4j;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,11 +33,9 @@
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Parameters;
 import org.kohsuke.args4j.spi.Setter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ProjectHandler extends OptionHandler<ProjectState> {
-  private static final Logger log = LoggerFactory.getLogger(ProjectHandler.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
@@ -89,7 +88,7 @@
     } catch (AuthException e) {
       throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage());
     } catch (PermissionBackendException | IOException e) {
-      log.warn("Cannot load project " + nameWithoutSuffix, e);
+      logger.atWarning().withCause(e).log("Cannot load project %s", nameWithoutSuffix);
       throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage());
     }
 
diff --git a/java/com/google/gerrit/server/audit/AuditService.java b/java/com/google/gerrit/server/audit/AuditService.java
index bd51824..9528670 100644
--- a/java/com/google/gerrit/server/audit/AuditService.java
+++ b/java/com/google/gerrit/server/audit/AuditService.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.audit;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -24,12 +25,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AuditService {
-  private static final Logger log = LoggerFactory.getLogger(AuditService.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<AuditListener> auditListeners;
   private final DynamicSet<GroupAuditListener> groupAuditListeners;
@@ -59,7 +58,7 @@
             GroupMemberAuditEvent.create(actor, updatedGroup, addedMembers, addedOn);
         auditListener.onAddMembers(event);
       } catch (RuntimeException e) {
-        log.error("failed to log add accounts to group event", e);
+        logger.atSevere().withCause(e).log("failed to log add accounts to group event");
       }
     }
   }
@@ -75,7 +74,7 @@
             GroupMemberAuditEvent.create(actor, updatedGroup, deletedMembers, deletedOn);
         auditListener.onDeleteMembers(event);
       } catch (RuntimeException e) {
-        log.error("failed to log delete accounts from group event", e);
+        logger.atSevere().withCause(e).log("failed to log delete accounts from group event");
       }
     }
   }
@@ -91,7 +90,7 @@
             GroupSubgroupAuditEvent.create(actor, updatedGroup, addedSubgroups, addedOn);
         auditListener.onAddSubgroups(event);
       } catch (RuntimeException e) {
-        log.error("failed to log add groups to group event", e);
+        logger.atSevere().withCause(e).log("failed to log add groups to group event");
       }
     }
   }
@@ -107,7 +106,7 @@
             GroupSubgroupAuditEvent.create(actor, updatedGroup, deletedSubgroups, deletedOn);
         auditListener.onDeleteSubgroups(event);
       } catch (RuntimeException e) {
-        log.error("failed to log delete groups from group event", e);
+        logger.atSevere().withCause(e).log("failed to log delete groups from group event");
       }
     }
   }
diff --git a/java/com/google/gerrit/server/auth/ldap/Helper.java b/java/com/google/gerrit/server/auth/ldap/Helper.java
index 16c1724..a53a8c2 100644
--- a/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountException;
@@ -24,6 +25,7 @@
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.util.ssl.BlindHostnameVerifier;
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -56,12 +58,10 @@
 import javax.security.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class Helper {
-  private static final Logger log = LoggerFactory.getLogger(Helper.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final String LDAP_UUID = "ldap:";
   static final String STARTTLS_PROPERTY = Helper.class.getName() + ".startTls";
@@ -139,6 +139,7 @@
       SSLSocketFactory sslfactory = null;
       if (!sslVerify) {
         sslfactory = (SSLSocketFactory) BlindSSLSocketFactory.getDefault();
+        tls.setHostnameVerifier(BlindHostnameVerifier.getInstance());
       }
       tls.negotiate(sslfactory);
       ctx.addToEnvironment(STARTTLS_PROPERTY, tls);
@@ -153,12 +154,12 @@
         tls.close();
       }
     } catch (IOException | NamingException e) {
-      log.warn("Cannot close LDAP startTls handle", e);
+      logger.atWarning().withCause(e).log("Cannot close LDAP startTls handle");
     }
     try {
       ctx.close();
     } catch (NamingException e) {
-      log.warn("Cannot close LDAP handle", e);
+      logger.atWarning().withCause(e).log("Cannot close LDAP handle");
     }
   }
 
@@ -196,7 +197,7 @@
       Throwables.throwIfInstanceOf(e.getException(), IOException.class);
       Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
       Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class);
-      log.warn("Internal error", e.getException());
+      logger.atWarning().withCause(e.getException()).log("Internal error");
       return null;
     } finally {
       ctx.logout();
@@ -343,7 +344,7 @@
             }
           }
         } catch (NamingException e) {
-          log.warn("Could not find group {}", groupDN, e);
+          logger.atWarning().withCause(e).log("Could not find group %s", groupDN);
         }
         cachedParentsDNs = dns.build();
         parentGroups.put(groupDN, cachedParentsDNs);
@@ -474,11 +475,10 @@
       try {
         return LdapType.guessType(ctx);
       } catch (NamingException e) {
-        log.warn(
-            "Cannot discover type of LDAP server at {},"
+        logger.atWarning().withCause(e).log(
+            "Cannot discover type of LDAP server at %s,"
                 + " assuming the server is RFC 2307 compliant.",
-            server,
-            e);
+            server);
         return LdapType.RFC_2307;
       }
     }
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
index 7f7152d..f31954e 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.auth.ldap;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.auth.AuthBackend;
@@ -33,12 +34,10 @@
 import javax.naming.directory.DirContext;
 import javax.security.auth.login.LoginException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Implementation of AuthBackend for the LDAP authentication system. */
 public class LdapAuthBackend implements AuthBackend {
-  private static final Logger log = LoggerFactory.getLogger(LdapAuthBackend.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Helper helper;
   private final AuthConfig authConfig;
@@ -90,13 +89,13 @@
         helper.close(ctx);
       }
     } catch (AccountException e) {
-      log.error("Cannot query LDAP to authenticate user", e);
+      logger.atSevere().withCause(e).log("Cannot query LDAP to authenticate user");
       throw new InvalidCredentialsException("Cannot query LDAP for account", e);
     } catch (IOException | NamingException e) {
-      log.error("Cannot query LDAP to authenticate user", e);
+      logger.atSevere().withCause(e).log("Cannot query LDAP to authenticate user");
       throw new AuthException("Cannot query LDAP for account", e);
     } catch (LoginException e) {
-      log.error("Cannot authenticate server via JAAS", e);
+      logger.atSevere().withCause(e).log("Cannot authenticate server via JAAS");
       throw new AuthException("Cannot query LDAP for account", e);
     }
   }
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 6e0356b..c338cd3 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -22,6 +22,7 @@
 
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
@@ -51,12 +52,10 @@
 import javax.naming.ldap.LdapName;
 import javax.naming.ldap.Rdn;
 import javax.security.auth.login.LoginException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Implementation of GroupBackend for the LDAP group system. */
 public class LdapGroupBackend implements GroupBackend {
-  static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String LDAP_NAME = "ldap/";
   private static final String GROUPNAME = "groupname";
@@ -103,7 +102,7 @@
         return cn;
       }
     } catch (InvalidNameException e) {
-      log.warn("Cannot parse LDAP dn for cn", e);
+      logger.atWarning().withCause(e).log("Cannot parse LDAP dn for cn");
     }
     return dn;
   }
@@ -127,7 +126,7 @@
           return null;
         }
       } catch (ExecutionException e) {
-        log.warn(String.format("Cannot lookup group %s in LDAP", groupDn), e);
+        logger.atWarning().withCause(e).log("Cannot lookup group %s in LDAP", groupDn);
         return null;
       }
     }
@@ -217,7 +216,7 @@
         helper.close(ctx);
       }
     } catch (IOException | NamingException | LoginException e) {
-      log.warn("Cannot query LDAP for groups matching requested name", e);
+      logger.atWarning().withCause(e).log("Cannot query LDAP for groups matching requested name");
     }
     return out;
   }
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java b/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
index 7bef2e7..7f0bd7b 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapGroupMembership.java
@@ -65,7 +65,10 @@
       try {
         membership = new ListGroupMembership(membershipCache.get(id));
       } catch (ExecutionException e) {
-        LdapGroupBackend.log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
+        LdapGroupBackend.logger
+            .atWarning()
+            .withCause(e)
+            .log("Cannot lookup membershipsOf %s in LDAP", id);
         membership = GroupMembership.EMPTY;
       }
     }
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 05228b4..3fbf049 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.auth.ldap;
 
-import static java.util.concurrent.TimeUnit.HOURS;
-
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
@@ -25,6 +23,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.Set;
 
@@ -37,18 +36,18 @@
   @Override
   protected void configure() {
     cache(GROUP_CACHE, String.class, new TypeLiteral<Set<AccountGroup.UUID>>() {})
-        .expireAfterWrite(1, HOURS)
+        .expireAfterWrite(Duration.ofHours(1))
         .loader(LdapRealm.MemberLoader.class);
 
     cache(USERNAME_CACHE, String.class, new TypeLiteral<Optional<Account.Id>>() {})
         .loader(LdapRealm.UserLoader.class);
 
     cache(GROUP_EXIST_CACHE, String.class, new TypeLiteral<Boolean>() {})
-        .expireAfterWrite(1, HOURS)
+        .expireAfterWrite(Duration.ofHours(1))
         .loader(LdapRealm.ExistenceLoader.class);
 
     cache(PARENT_GROUPS_CACHE, String.class, new TypeLiteral<ImmutableSet<String>>() {})
-        .expireAfterWrite(1, HOURS);
+        .expireAfterWrite(Duration.ofHours(1));
 
     bind(Helper.class);
     bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index b83c7b2..8d12d32 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Strings;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.extensions.client.AccountFieldName;
@@ -56,12 +57,10 @@
 import javax.naming.directory.DirContext;
 import javax.security.auth.login.LoginException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class LdapRealm extends AbstractRealm {
-  private static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
   static final String USERNAME = "username";
@@ -196,7 +195,7 @@
       String configOption, String suppliedValue, boolean disabledByBackend) {
     if (disabledByBackend && !Strings.isNullOrEmpty(suppliedValue)) {
       String msg = String.format("LDAP backend doesn't support: ldap.%s", configOption);
-      log.error(msg);
+      logger.atSevere().log(msg);
       throw new IllegalArgumentException(msg);
     }
   }
@@ -290,10 +289,10 @@
         helper.close(ctx);
       }
     } catch (IOException | NamingException e) {
-      log.error("Cannot query LDAP to authenticate user", e);
+      logger.atSevere().withCause(e).log("Cannot query LDAP to authenticate user");
       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     } catch (LoginException e) {
-      log.error("Cannot authenticate server via JAAS", e);
+      logger.atSevere().withCause(e).log("Cannot authenticate server via JAAS");
       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     }
   }
@@ -312,7 +311,7 @@
       Optional<Account.Id> id = usernameCache.get(accountName);
       return id != null ? id.orElse(null) : null;
     } catch (ExecutionException e) {
-      log.warn(String.format("Cannot lookup account %s in LDAP", accountName), e);
+      logger.atWarning().withCause(e).log("Cannot lookup account %s in LDAP", accountName);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index f380051..13a09a1 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -32,7 +32,6 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import java.io.IOException;
 
 @Singleton
 public class OAuthTokenCache {
@@ -70,12 +69,7 @@
 
     @Override
     public OAuthToken deserialize(byte[] in) {
-      OAuthTokenProto proto;
-      try {
-        proto = OAuthTokenProto.parseFrom(in);
-      } catch (IOException e) {
-        throw new IllegalArgumentException("failed to deserialize OAuthToken");
-      }
+      OAuthTokenProto proto = ProtoCacheSerializers.parseUnchecked(OAuthTokenProto.parser(), in);
       return new OAuthToken(
           proto.getToken(),
           proto.getSecret(),
diff --git a/java/com/google/gerrit/server/cache/CacheBinding.java b/java/com/google/gerrit/server/cache/CacheBinding.java
index 1c0d7ef..9d90d073 100644
--- a/java/com/google/gerrit/server/cache/CacheBinding.java
+++ b/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -16,15 +16,18 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.Weigher;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 /** Configure a cache declared within a {@link CacheModule} instance. */
 public interface CacheBinding<K, V> {
   /** Set the total size of the cache. */
   CacheBinding<K, V> maximumWeight(long weight);
 
-  /** Set the time an element lives before being expired. */
-  CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
+  /** Set the time an element lives after last write before being expired. */
+  CacheBinding<K, V> expireAfterWrite(Duration duration);
+
+  /** Set the time an element lives after last access before being expired. */
+  CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
 
   /** Populate the cache with items from the CacheLoader. */
   CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
diff --git a/java/com/google/gerrit/server/cache/CacheDef.java b/java/com/google/gerrit/server/cache/CacheDef.java
index adb7585..d0c633e 100644
--- a/java/com/google/gerrit/server/cache/CacheDef.java
+++ b/java/com/google/gerrit/server/cache/CacheDef.java
@@ -18,7 +18,7 @@
 import com.google.common.cache.Weigher;
 import com.google.gerrit.common.Nullable;
 import com.google.inject.TypeLiteral;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 public interface CacheDef<K, V> {
   /**
@@ -45,7 +45,10 @@
   long maximumWeight();
 
   @Nullable
-  Long expireAfterWrite(TimeUnit unit);
+  Duration expireAfterWrite();
+
+  @Nullable
+  Duration expireFromMemoryAfterAccess();
 
   @Nullable
   Weigher<K, V> weigher();
diff --git a/java/com/google/gerrit/server/cache/CacheProvider.java b/java/com/google/gerrit/server/cache/CacheProvider.java
index ce94bfa..2bd570e 100644
--- a/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
@@ -27,7 +26,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>, CacheDef<K, V> {
   private final CacheModule module;
@@ -36,7 +35,8 @@
   private final TypeLiteral<V> valType;
   private String configKey;
   private long maximumWeight;
-  private Long expireAfterWrite;
+  private Duration expireAfterWrite;
+  private Duration expireFromMemoryAfterAccess;
   private Provider<CacheLoader<K, V>> loader;
   private Provider<Weigher<K, V>> weigher;
 
@@ -69,9 +69,16 @@
   }
 
   @Override
-  public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit unit) {
+  public CacheBinding<K, V> expireAfterWrite(Duration duration) {
     checkNotFrozen();
-    expireAfterWrite = SECONDS.convert(duration, unit);
+    expireAfterWrite = duration;
+    return this;
+  }
+
+  @Override
+  public CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration) {
+    checkNotFrozen();
+    expireFromMemoryAfterAccess = duration;
     return this;
   }
 
@@ -126,8 +133,14 @@
 
   @Override
   @Nullable
-  public Long expireAfterWrite(TimeUnit unit) {
-    return expireAfterWrite != null ? unit.convert(expireAfterWrite, SECONDS) : null;
+  public Duration expireAfterWrite() {
+    return expireAfterWrite;
+  }
+
+  @Override
+  @Nullable
+  public Duration expireFromMemoryAfterAccess() {
+    return expireFromMemoryAfterAccess;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/cache/PerThreadCache.java b/java/com/google/gerrit/server/cache/PerThreadCache.java
index 0881d59..b4f79d1 100644
--- a/java/com/google/gerrit/server/cache/PerThreadCache.java
+++ b/java/com/google/gerrit/server/cache/PerThreadCache.java
@@ -51,11 +51,11 @@
 public class PerThreadCache implements AutoCloseable {
   private static final ThreadLocal<PerThreadCache> CACHE = new ThreadLocal<>();
   /**
-   * Cache at maximum 50 values per thread. This value was chosen arbitrarily. Some endpoints (like
+   * Cache at maximum 25 values per thread. This value was chosen arbitrarily. Some endpoints (like
    * ListProjects) break the assumption that the data cached in a request is limited. To prevent
    * this class from accumulating an unbound number of objects, we enforce this limit.
    */
-  private static final int PER_THREAD_CACHE_SIZE = 50;
+  private static final int PER_THREAD_CACHE_SIZE = 25;
 
   /**
    * Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
index 794d3bb..0239ea2 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
@@ -16,7 +16,7 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.Weigher;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 /** Configure a persistent cache declared within a {@link CacheModule} instance. */
 public interface PersistentCacheBinding<K, V> extends CacheBinding<K, V> {
@@ -24,7 +24,7 @@
   PersistentCacheBinding<K, V> maximumWeight(long weight);
 
   @Override
-  PersistentCacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
+  PersistentCacheBinding<K, V> expireAfterWrite(Duration duration);
 
   @Override
   PersistentCacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
index 46a9e61..2db9e56 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
@@ -24,7 +24,7 @@
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import java.io.Serializable;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 class PersistentCacheProvider<K, V> extends CacheProvider<K, V>
     implements Provider<Cache<K, V>>, PersistentCacheBinding<K, V>, PersistentCacheDef<K, V> {
@@ -53,8 +53,8 @@
   }
 
   @Override
-  public PersistentCacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits) {
-    return (PersistentCacheBinding<K, V>) super.expireAfterWrite(duration, durationUnits);
+  public PersistentCacheBinding<K, V> expireAfterWrite(Duration duration) {
+    return (PersistentCacheBinding<K, V>) super.expireAfterWrite(duration);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java b/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
index 9fe6b83..c6fc0b9 100644
--- a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
+++ b/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
@@ -14,11 +14,16 @@
 
 package com.google.gerrit.server.cache;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.CodedOutputStream;
 import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
 import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
 
 /** Static utilities for writing protobuf-based {@link CacheSerializer} implementations. */
 public class ProtoCacheSerializers {
@@ -68,5 +73,55 @@
     }
   }
 
+  /**
+   * Parses a byte array to a protobuf message.
+   *
+   * @param parser parser for the proto type.
+   * @param in byte array with the message contents.
+   * @return parsed proto.
+   */
+  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, byte[] in) {
+    try {
+      return parser.parseFrom(in);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing byte array to proto", e);
+    }
+  }
+
+  /**
+   * Helper for serializing {@link ObjectId} instances to/from protobuf fields.
+   *
+   * <p>Reuse a single instance's {@link #toByteString(ObjectId)} and {@link
+   * #fromByteString(ByteString)} within a single {@link CacheSerializer#serialize} or {@link
+   * CacheSerializer#deserialize} method body to minimize allocation of temporary buffers.
+   *
+   * <p><strong>Note:</strong> This class is not threadsafe. Instances must not be stored in {@link
+   * CacheSerializer} fields if the serializer instances will be used from multiple threads.
+   */
+  public static class ObjectIdConverter {
+    public static ObjectIdConverter create() {
+      return new ObjectIdConverter();
+    }
+
+    private final byte[] buf = new byte[OBJECT_ID_LENGTH];
+
+    private ObjectIdConverter() {}
+
+    public ByteString toByteString(ObjectId id) {
+      id.copyRawTo(buf, 0);
+      return ByteString.copyFrom(buf);
+    }
+
+    public ObjectId fromByteString(ByteString in) {
+      checkArgument(
+          in.size() == OBJECT_ID_LENGTH,
+          "expected ByteString of length %s: %s",
+          OBJECT_ID_LENGTH,
+          in);
+      in.copyTo(buf, 0);
+      return ObjectId.fromRaw(buf);
+    }
+  }
+
   private ProtoCacheSerializers() {}
 }
diff --git a/java/com/google/gerrit/server/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index f8d0105..fc57a11 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -3,14 +3,15 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/server",
         "//lib:guava",
         "//lib:h2",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index 7b3da31..78de67dd 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -16,11 +16,12 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.Weigher;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.PersistentCacheDef;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
 import com.google.inject.TypeLiteral;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 class H2CacheDefProxy<K, V> implements PersistentCacheDef<K, V> {
   private final PersistentCacheDef<K, V> source;
@@ -30,8 +31,15 @@
   }
 
   @Override
-  public Long expireAfterWrite(TimeUnit unit) {
-    return source.expireAfterWrite(unit);
+  @Nullable
+  public Duration expireAfterWrite() {
+    return source.expireAfterWrite();
+  }
+
+  @Override
+  @Nullable
+  public Duration expireFromMemoryAfterAccess() {
+    return source.expireFromMemoryAfterAccess();
   }
 
   @SuppressWarnings("unchecked")
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 451540d..9abccbc 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -17,6 +17,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -42,12 +43,10 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final MemoryCacheFactory memCacheFactory;
   private final Config config;
@@ -99,15 +98,15 @@
       try {
         Files.createDirectories(loc);
       } catch (IOException e) {
-        log.warn("Can't create disk cache: " + loc.toAbsolutePath());
+        logger.atWarning().log("Can't create disk cache: %s", loc.toAbsolutePath());
         return null;
       }
     }
     if (!Files.isWritable(loc)) {
-      log.warn("Can't write to disk cache: " + loc.toAbsolutePath());
+      logger.atWarning().log("Can't write to disk cache: %s", loc.toAbsolutePath());
       return null;
     }
-    log.info("Enabling disk cache " + loc.toAbsolutePath());
+    logger.atInfo().log("Enabling disk cache %s", loc.toAbsolutePath());
     return loc;
   }
 
@@ -132,16 +131,16 @@
         List<Runnable> pending = executor.shutdownNow();
         if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
           if (pending != null && !pending.isEmpty()) {
-            log.info(String.format("Finishing %d disk cache updates", pending.size()));
+            logger.atInfo().log("Finishing %d disk cache updates", pending.size());
             for (Runnable update : pending) {
               update.run();
             }
           }
         } else {
-          log.info("Timeout waiting for disk cache to close");
+          logger.atInfo().log("Timeout waiting for disk cache to close");
         }
       } catch (InterruptedException e) {
-        log.warn("Interrupted waiting for disk cache to shutdown");
+        logger.atWarning().log("Interrupted waiting for disk cache to shutdown");
       }
     }
     synchronized (caches) {
@@ -216,7 +215,6 @@
     if (h2AutoServer) {
       url.append(";AUTO_SERVER=TRUE");
     }
-    Long expireAfterWrite = def.expireAfterWrite(TimeUnit.SECONDS);
     return new SqlStore<>(
         url.toString(),
         def.keyType(),
@@ -224,6 +222,6 @@
         def.valueSerializer(),
         def.version(),
         maxSize,
-        expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
+        def.expireAfterWrite());
   }
 }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 7db1a35..d7baee2 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -21,7 +21,9 @@
 import com.google.common.cache.CacheStats;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.BloomFilter;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.PersistentCache;
@@ -34,6 +36,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.sql.Timestamp;
+import java.time.Duration;
 import java.util.Calendar;
 import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -46,8 +49,6 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import org.h2.jdbc.JdbcSQLException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Hybrid in-memory and database backed cache built on H2.
@@ -71,7 +72,7 @@
  * @see H2CacheFactory
  */
 public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements PersistentCache {
-  private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final ImmutableSet<String> OLD_CLASS_NAMES =
       ImmutableSet.of("com.google.gerrit.server.change.ChangeKind");
@@ -255,7 +256,7 @@
     private final CacheSerializer<V> valueSerializer;
     private final int version;
     private final long maxSize;
-    private final long expireAfterWrite;
+    @Nullable private final Duration expireAfterWrite;
     private final BlockingQueue<SqlHandle> handles;
     private final AtomicLong hitCount = new AtomicLong();
     private final AtomicLong missCount = new AtomicLong();
@@ -269,7 +270,7 @@
         CacheSerializer<V> valueSerializer,
         int version,
         long maxSize,
-        long expireAfterWrite) {
+        @Nullable Duration expireAfterWrite) {
       this.url = jdbcUrl;
       this.keyType = createKeyType(keyType, keySerializer);
       this.valueSerializer = valueSerializer;
@@ -347,11 +348,10 @@
             // most likely bumping serialVersionUID rather than using the new versioning in the
             // CacheBinding.  That's ok; we'll continue to support both for now.
             // TODO(dborowitz): Remove this case when Java serialization is no longer used.
-            log.warn(
-                "Entries cached for "
-                    + url
-                    + " have an incompatible class and can't be deserialized. "
-                    + "Cache is flushed.");
+            logger.atWarning().log(
+                "Entries cached for %s have an incompatible class and can't be deserialized. "
+                    + "Cache is flushed.",
+                url);
             invalidateAll();
           } else {
             throw e;
@@ -359,7 +359,7 @@
         }
         return b;
       } catch (IOException | SQLException e) {
-        log.warn("Cannot build BloomFilter for " + url + ": " + e.getMessage());
+        logger.atWarning().log("Cannot build BloomFilter for %s: %s", url, e.getMessage());
         c = close(c);
         return null;
       } finally {
@@ -404,7 +404,7 @@
         }
       } catch (IOException | SQLException e) {
         if (!isOldClassNameError(e)) {
-          log.warn("Cannot read cache " + url + " for " + key, e);
+          logger.atWarning().withCause(e).log("Cannot read cache %s for %s", url, key);
         }
         c = close(c);
         return null;
@@ -423,11 +423,11 @@
     }
 
     private boolean expired(Timestamp created) {
-      if (expireAfterWrite == 0) {
+      if (expireAfterWrite == null) {
         return false;
       }
-      long age = TimeUtil.nowMs() - created.getTime();
-      return 1000 * expireAfterWrite < age;
+      Duration age = Duration.between(created.toInstant(), TimeUtil.now());
+      return age.compareTo(expireAfterWrite) > 0;
     }
 
     private void touch(SqlHandle c, K key) throws IOException, SQLException {
@@ -475,7 +475,7 @@
           c.put.clearParameters();
         }
       } catch (IOException | SQLException e) {
-        log.warn("Cannot put into cache " + url, e);
+        logger.atWarning().withCause(e).log("Cannot put into cache %s", url);
         c = close(c);
       } finally {
         release(c);
@@ -488,7 +488,7 @@
         c = acquire();
         invalidate(c, key);
       } catch (IOException | SQLException e) {
-        log.warn("Cannot invalidate cache " + url, e);
+        logger.atWarning().withCause(e).log("Cannot invalidate cache %s", url);
         c = close(c);
       } finally {
         release(c);
@@ -517,7 +517,7 @@
         }
         bloomFilter = newBloomFilter();
       } catch (SQLException e) {
-        log.warn("Cannot invalidate cache " + url, e);
+        logger.atWarning().withCause(e).log("Cannot invalidate cache %s", url);
         c = close(c);
       } finally {
         release(c);
@@ -531,8 +531,8 @@
         try (PreparedStatement ps = c.conn.prepareStatement("DELETE FROM data WHERE version!=?")) {
           ps.setInt(1, version);
           int oldEntries = ps.executeUpdate();
-          log.info(
-              "Pruned {} entries not matching version {} from cache {}", oldEntries, version, url);
+          logger.atInfo().log(
+              "Pruned %d entries not matching version %d from cache %s", oldEntries, version, url);
         }
         try (Statement s = c.conn.createStatement()) {
           // Compute size without restricting to version (although obsolete data was just pruned
@@ -560,7 +560,7 @@
           }
         }
       } catch (IOException | SQLException e) {
-        log.warn("Cannot prune cache " + url, e);
+        logger.atWarning().withCause(e).log("Cannot prune cache %s", url);
         c = close(c);
       } finally {
         release(c);
@@ -582,7 +582,7 @@
           }
         }
       } catch (SQLException e) {
-        log.warn("Cannot get DiskStats for " + url, e);
+        logger.atWarning().withCause(e).log("Cannot get DiskStats for %s", url);
         c = close(c);
       } finally {
         release(c);
@@ -653,7 +653,7 @@
         try {
           conn.close();
         } catch (SQLException e) {
-          log.warn("Cannot close connection to " + url, e);
+          logger.atWarning().withCause(e).log("Cannot close connection to %s", url);
         } finally {
           conn = null;
         }
@@ -665,7 +665,7 @@
         try {
           ps.close();
         } catch (SQLException e) {
-          log.warn("Cannot close statement for " + url, e);
+          logger.atWarning().withCause(e).log("Cannot close statement for %s", url);
         }
       }
       return null;
diff --git a/java/com/google/gerrit/server/cache/mem/BUILD b/java/com/google/gerrit/server/cache/mem/BUILD
index ef297f1..4106714 100644
--- a/java/com/google/gerrit/server/cache/mem/BUILD
+++ b/java/com/google/gerrit/server/cache/mem/BUILD
@@ -3,6 +3,7 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
         "//lib:guava",
diff --git a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index f16912a..ad1d396 100644
--- a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -14,19 +14,23 @@
 
 package com.google.gerrit.server.cache.mem;
 
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.cache.Weigher;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.cache.CacheDef;
 import com.google.gerrit.server.cache.ForwardingRemovalListener;
 import com.google.gerrit.server.cache.MemoryCacheFactory;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 import org.eclipse.jgit.lib.Config;
 
 class DefaultMemoryCacheFactory implements MemoryCacheFactory {
@@ -66,19 +70,38 @@
     }
     builder.weigher(weigher);
 
-    Long age = def.expireAfterWrite(TimeUnit.SECONDS);
+    Duration expireAfterWrite = def.expireAfterWrite();
     if (has(def.configKey(), "maxAge")) {
       builder.expireAfterWrite(
           ConfigUtil.getTimeUnit(
-              cfg, "cache", def.configKey(), "maxAge", age != null ? age : 0, TimeUnit.SECONDS),
-          TimeUnit.SECONDS);
-    } else if (age != null) {
-      builder.expireAfterWrite(age, TimeUnit.SECONDS);
+              cfg, "cache", def.configKey(), "maxAge", toSeconds(expireAfterWrite), SECONDS),
+          SECONDS);
+    } else if (expireAfterWrite != null) {
+      builder.expireAfterWrite(expireAfterWrite.toNanos(), NANOSECONDS);
+    }
+
+    Duration expireAfterAccess = def.expireFromMemoryAfterAccess();
+    if (has(def.configKey(), "expireFromMemoryAfterAccess")) {
+      builder.expireAfterAccess(
+          ConfigUtil.getTimeUnit(
+              cfg,
+              "cache",
+              def.configKey(),
+              "expireFromMemoryAfterAccess",
+              toSeconds(expireAfterAccess),
+              SECONDS),
+          SECONDS);
+    } else if (expireAfterAccess != null) {
+      builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
     }
 
     return builder;
   }
 
+  private static long toSeconds(@Nullable Duration duration) {
+    return duration != null ? duration.getSeconds() : 0;
+  }
+
   private boolean has(String name, String var) {
     return !Strings.isNullOrEmpty(cfg.getString("cache", name, var));
   }
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index 2543f76..5affd5c 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -38,11 +39,9 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class AbandonOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(AbandonOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final ChangeMessagesUtil cmUtil;
@@ -133,7 +132,7 @@
       cm.setAccountsToNotify(accountsToNotify);
       cm.send();
     } catch (Exception e) {
-      log.error("Cannot email update for change " + change.getId(), e);
+      logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
     }
     changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notifyHandling);
   }
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index 9866ea9..f505f6d 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.InternalUser;
@@ -32,12 +33,10 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AbandonUtil {
-  private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ChangeCleanupConfig cfg;
   private final Provider<ChangeQueryProcessor> queryProvider;
@@ -93,12 +92,13 @@
             msg.append(" ").append(change.getId().get());
           }
           msg.append(".");
-          log.error(msg.toString(), e);
+          logger.atSevere().withCause(e).log(msg.toString());
         }
       }
-      log.info(String.format("Auto-Abandoned %d of %d changes.", count, changesToAbandon.size()));
+      logger.atInfo().log("Auto-Abandoned %d of %d changes.", count, changesToAbandon.size());
     } catch (QueryParseException | OrmException e) {
-      log.error("Failed to query inactive open changes for auto-abandoning.", e);
+      logger.atSevere().withCause(e).log(
+          "Failed to query inactive open changes for auto-abandoning.");
     }
   }
 
@@ -116,11 +116,10 @@
       if (!changesToAbandon.isEmpty()) {
         validChanges.add(cd);
       } else {
-        log.debug(
-            "Change data with id \"{}\" does not satisfy the query \"{}\""
+        logger.atFine().log(
+            "Change data with id \"%s\" does not satisfy the query \"%s\""
                 + " any more, hence skipping it in clean up",
-            cd.getId(),
-            query);
+            cd.getId(), query);
       }
     }
     return validChanges;
diff --git a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
index a887760..b24d3ce 100644
--- a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
+++ b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -25,12 +26,10 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Runnable to enable scheduling change cleanups to run periodically */
 public class ChangeCleanupRunner implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(ChangeCleanupRunner.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends LifecycleModule {
     @Override
@@ -76,7 +75,7 @@
 
   @Override
   public void run() {
-    log.info("Running change cleanups.");
+    logger.atInfo().log("Running change cleanups.");
     try (ManualRequestContext ctx = oneOffRequestContext.open()) {
       // abandonInactiveOpenChanges skips failures instead of throwing, so retrying will never
       // actually happen. For the purposes of this class that is fine: they'll get tried again the
@@ -87,7 +86,7 @@
             return null;
           });
     } catch (RestApiException | UpdateException | OrmException e) {
-      log.error("Failed to cleanup changes.", e);
+      logger.atSevere().withCause(e).log("Failed to cleanup changes.");
     }
   }
 
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 34cbea3..c6fe93b 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -23,6 +23,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -80,16 +81,14 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ChangeInserter implements InsertChangeOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public interface Factory {
     ChangeInserter create(Change.Id cid, ObjectId commitId, String refName);
   }
 
-  private static final Logger log = LoggerFactory.getLogger(ChangeInserter.class);
-
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final PatchSetInfoFactory patchSetInfoFactory;
@@ -468,11 +467,9 @@
                         .test(ChangePermission.READ)
                     && projectState.statePermitsRead();
               } catch (PermissionBackendException e) {
-                log.warn(
-                    String.format(
-                        "Failed to check if account %d can see change %d",
-                        accountId.get(), notes.getChangeId().get()),
-                    e);
+                logger.atWarning().withCause(e).log(
+                    "Failed to check if account %d can see change %d",
+                    accountId.get(), notes.getChangeId().get());
                 return false;
               }
             })
@@ -497,7 +494,8 @@
                 cm.addExtraCC(extraCC);
                 cm.send();
               } catch (Exception e) {
-                log.error("Cannot send email for new change " + change.getId(), e);
+                logger.atSevere().withCause(e).log(
+                    "Cannot send email for new change %s", change.getId());
               }
             }
 
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 73a7634..8629898 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -57,6 +57,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
@@ -165,8 +166,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
@@ -175,7 +174,7 @@
  * ChangeData} objects from different sources.
  */
 public class ChangeJson {
-  private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
       ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().build();
@@ -502,7 +501,8 @@
                 ensureLoaded(Collections.singleton(cd));
                 return Optional.of(format(cd, Optional.empty(), false));
               } catch (OrmException | RuntimeException e) {
-                log.warn("Omitting corrupt change " + cd.getId() + " from results", e);
+                logger.atWarning().withCause(e).log(
+                    "Omitting corrupt change %s from results", cd.getId());
                 return Optional.empty();
               }
             });
@@ -517,7 +517,7 @@
           try {
             c.call().ifPresent(result::add);
           } catch (Exception e) {
-            log.warn("Omitting change due to exception", e);
+            logger.atWarning().withCause(e).log("Omitting change due to exception");
           }
         }
         return result;
@@ -542,7 +542,7 @@
       notes = cd.notes();
     } catch (OrmException e) {
       String msg = "Error loading change";
-      log.warn(msg + " " + cd.getId(), e);
+      logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
       ChangeInfo info = new ChangeInfo();
       info._number = cd.getId().get();
       ProblemInfo p = new ProblemInfo();
@@ -914,7 +914,7 @@
           tag = psa.getTag();
           date = psa.getGranted();
           if (psa.isPostSubmit()) {
-            log.warn("unexpected post-submit approval on open change: {}", psa);
+            logger.atWarning().log("unexpected post-submit approval on open change: %s", psa);
           }
         } else {
           // Either the user cannot vote on this label, or they were added as a
@@ -1559,7 +1559,7 @@
     }
     ProjectState projectState = projectCache.checkedGet(cd.project());
     if (projectState == null) {
-      log.error("project state for project " + cd.project() + " is null");
+      logger.atSevere().log("project state for project %s is null", cd.project());
       return false;
     }
     return projectState.statePermitsRead();
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index a4eb90f..24685af 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -15,12 +15,13 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.common.collect.FluentIterable;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
@@ -31,6 +32,7 @@
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.EnumCacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -41,8 +43,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -51,19 +51,17 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ChangeKindCacheImpl implements ChangeKindCache {
-  private static final Logger log = LoggerFactory.getLogger(ChangeKindCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String ID_CACHE = "change_kind";
 
@@ -106,11 +104,11 @@
         ObjectId prior,
         ObjectId next) {
       try {
-        Key key = new Key(prior, next, useRecursiveMerge);
+        Key key = Key.create(prior, next, useRecursiveMerge);
         return new Loader(key, repoManager, project, rw, repoConfig).call();
       } catch (IOException e) {
-        log.warn(
-            "Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
+        logger.atWarning().withCause(e).log(
+            "Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
         return ChangeKind.REWORK;
       }
     }
@@ -127,78 +125,44 @@
     }
   }
 
-  public static class Key {
-    private transient ObjectId prior;
-    private transient ObjectId next;
-    private transient String strategyName;
-
-    private Key(ObjectId prior, ObjectId next, boolean useRecursiveMerge) {
-      checkNotNull(next, "next");
-      String strategyName = MergeUtil.mergeStrategyName(true, useRecursiveMerge);
-      this.prior = prior.copy();
-      this.next = next.copy();
-      this.strategyName = strategyName;
+  @AutoValue
+  public abstract static class Key {
+    public static Key create(AnyObjectId prior, AnyObjectId next, String strategyName) {
+      return new AutoValue_ChangeKindCacheImpl_Key(prior.copy(), next.copy(), strategyName);
     }
 
-    public Key(ObjectId prior, ObjectId next, String strategyName) {
-      this.prior = prior;
-      this.next = next;
-      this.strategyName = strategyName;
+    private static Key create(AnyObjectId prior, AnyObjectId next, boolean useRecursiveMerge) {
+      return create(prior, next, MergeUtil.mergeStrategyName(true, useRecursiveMerge));
     }
 
-    public ObjectId getPrior() {
-      return prior;
-    }
+    public abstract ObjectId prior();
 
-    public ObjectId getNext() {
-      return next;
-    }
+    public abstract ObjectId next();
 
-    public String getStrategyName() {
-      return strategyName;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof Key) {
-        Key k = (Key) o;
-        return Objects.equals(prior, k.prior)
-            && Objects.equals(next, k.next)
-            && Objects.equals(strategyName, k.strategyName);
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(prior, next, strategyName);
-    }
+    public abstract String strategyName();
 
     @VisibleForTesting
     static class Serializer implements CacheSerializer<Key> {
       @Override
       public byte[] serialize(Key object) {
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        ChangeKindKeyProto.Builder b = ChangeKindKeyProto.newBuilder();
-        object.getPrior().copyRawTo(buf, 0);
-        b.setPrior(ByteString.copyFrom(buf));
-        object.getNext().copyRawTo(buf, 0);
-        b.setNext(ByteString.copyFrom(buf));
-        b.setStrategyName(object.getStrategyName());
-        return ProtoCacheSerializers.toByteArray(b.build());
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
+        return ProtoCacheSerializers.toByteArray(
+            ChangeKindKeyProto.newBuilder()
+                .setPrior(idConverter.toByteString(object.prior()))
+                .setNext(idConverter.toByteString(object.next()))
+                .setStrategyName(object.strategyName())
+                .build());
       }
 
       @Override
       public Key deserialize(byte[] in) {
-        try {
-          ChangeKindKeyProto proto = ChangeKindKeyProto.parseFrom(in);
-          return new Key(
-              ObjectId.fromRaw(proto.getPrior().toByteArray()),
-              ObjectId.fromRaw(proto.getNext().toByteArray()),
-              proto.getStrategyName());
-        } catch (InvalidProtocolBufferException e) {
-          throw new IllegalArgumentException("Failed to deserialize object", e);
-        }
+        ChangeKindKeyProto proto =
+            ProtoCacheSerializers.parseUnchecked(ChangeKindKeyProto.parser(), in);
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
+        return create(
+            idConverter.fromByteString(proto.getPrior()),
+            idConverter.fromByteString(proto.getNext()),
+            proto.getStrategyName());
       }
     }
   }
@@ -231,7 +195,7 @@
     @SuppressWarnings("resource") // Resources are manually managed.
     @Override
     public ChangeKind call() throws IOException {
-      if (Objects.equals(key.prior, key.next)) {
+      if (Objects.equals(key.prior(), key.next())) {
         return ChangeKind.NO_CODE_CHANGE;
       }
 
@@ -244,9 +208,9 @@
         config = repo.getConfig();
       }
       try {
-        RevCommit prior = rw.parseCommit(key.prior);
+        RevCommit prior = rw.parseCommit(key.prior());
         rw.parseBody(prior);
-        RevCommit next = rw.parseCommit(key.next);
+        RevCommit next = rw.parseCommit(key.next());
         rw.parseBody(next);
 
         if (!next.getFullMessage().equals(prior.getFullMessage())) {
@@ -277,7 +241,7 @@
         // having the same tree as would exist when the prior commit is
         // cherry-picked onto the next commit's new first parent.
         try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
-          ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName);
+          ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName());
           merger.setBase(prior.getParent(0));
           if (merger.merge(next.getParent(0), prior)
               && merger.getResultTreeId().equals(next.getTree())) {
@@ -321,7 +285,7 @@
     }
 
     private static boolean isSameDeltaAndTree(RevCommit prior, RevCommit next) {
-      if (next.getTree() != prior.getTree()) {
+      if (!Objects.equals(next.getTree(), prior.getTree())) {
         return false;
       }
 
@@ -334,7 +298,7 @@
       // Make sure that the prior/next delta is the same - not just the tree.
       // This is done by making sure that the parent trees are equal.
       for (int i = 0; i < prior.getParentCount(); i++) {
-        if (next.getParent(i).getTree() != prior.getParent(i).getTree()) {
+        if (!Objects.equals(next.getParent(i).getTree(), prior.getParent(i).getTree())) {
           return false;
         }
       }
@@ -347,7 +311,7 @@
     public int weigh(Key key, ChangeKind changeKind) {
       return 16
           + 2 * 36
-          + 2 * key.strategyName.length() // Size of Key, 64 bit JVM
+          + 2 * key.strategyName().length() // Size of Key, 64 bit JVM
           + 2 * changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
     }
   }
@@ -377,10 +341,11 @@
       ObjectId prior,
       ObjectId next) {
     try {
-      Key key = new Key(prior, next, useRecursiveMerge);
+      Key key = Key.create(prior, next, useRecursiveMerge);
       return cache.get(key, new Loader(key, repoManager, project, rw, repoConfig));
     } catch (ExecutionException e) {
-      log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
+      logger.atWarning().withCause(e).log(
+          "Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
       return ChangeKind.REWORK;
     }
   }
@@ -432,12 +397,9 @@
         }
       } catch (OrmException e) {
         // Do nothing; assume we have a complex change
-        log.warn(
-            "Unable to get change kind for patchSet "
-                + patch.getPatchSetId()
-                + "of change "
-                + change.getId(),
-            e);
+        logger.atWarning().withCause(e).log(
+            "Unable to get change kind for patchSet %s of change %s",
+            patch.getPatchSetId(), change.getId());
       }
     }
     return kind;
@@ -462,12 +424,9 @@
                 cache, rw, repo.getConfig(), changeDataFactory.create(db, change), patch);
       } catch (IOException e) {
         // Do nothing; assume we have a complex change
-        log.warn(
-            "Unable to get change kind for patchSet "
-                + patch.getPatchSetId()
-                + "of change "
-                + change.getChangeId(),
-            e);
+        logger.atWarning().withCause(e).log(
+            "Unable to get change kind for patchSet %s of change %s",
+            patch.getPatchSetId(), change.getChangeId());
       }
     }
     return kind;
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 69193d3..ef8b2f9 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.extensions.restapi.RestResource;
@@ -49,11 +50,9 @@
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ChangeResource implements RestResource, HasETag {
-  private static final Logger log = LoggerFactory.getLogger(ChangeResource.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /**
    * JSON format version number for ETag computations.
@@ -196,7 +195,7 @@
     try {
       projectStateTree = projectCache.checkedGet(getProject()).tree();
     } catch (IOException e) {
-      log.error(String.format("could not load project %s while computing etag", getProject()));
+      logger.atSevere().log("could not load project %s while computing etag", getProject());
       projectStateTree = ImmutableList.of();
     }
 
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index c58cde7..0aa6c2f 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
@@ -80,8 +81,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Checks changes for various kinds of inconsistency and corruption.
@@ -89,7 +88,7 @@
  * <p>A single instance may be reused for checking multiple changes, but not concurrently.
  */
 public class ConsistencyChecker {
-  private static final Logger log = LoggerFactory.getLogger(ConsistencyChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @AutoValue
   public abstract static class Result {
@@ -202,7 +201,7 @@
   }
 
   private Result logAndReturnOneProblem(Exception e, ChangeNotes notes, String problem) {
-    log.warn("Error checking change " + notes.getChangeId(), e);
+    logger.atWarning().withCause(e).log("Error checking change %s", notes.getChangeId());
     return Result.create(notes, ImmutableList.of(problem(problem)));
   }
 
@@ -582,7 +581,7 @@
       bu.addOp(notes.getChangeId(), new FixMergedOp(p));
       bu.execute();
     } catch (UpdateException | RestApiException e) {
-      log.warn("Error marking " + notes.getChangeId() + "as merged", e);
+      logger.atWarning().withCause(e).log("Error marking %s as merged", notes.getChangeId());
       p.status = Status.FIX_FAILED;
       p.outcome = "Error updating status to merged";
     }
@@ -623,7 +622,7 @@
       }
     } catch (IOException e) {
       String msg = "Error fixing patch set ref";
-      log.warn(msg + ' ' + ps.getId().toRefName(), e);
+      logger.atWarning().withCause(e).log("%s %s", msg, ps.getId().toRefName());
       p.status = Status.FIX_FAILED;
       p.outcome = msg;
     }
@@ -645,7 +644,7 @@
       }
     } catch (UpdateException | RestApiException e) {
       String msg = "Error deleting patch set";
-      log.warn(msg + " of change " + ops.get(0).psId.getParentKey(), e);
+      logger.atWarning().withCause(e).log("%s of change %s", msg, ops.get(0).psId.getParentKey());
       for (DeletePatchSetFromDbOp op : ops) {
         // Overwrite existing statuses that were set before the transaction was
         // rolled back.
@@ -777,7 +776,8 @@
   }
 
   private void warn(Throwable t) {
-    log.warn("Error in consistency check of change " + notes.getChangeId(), t);
+    logger.atWarning().withCause(t).log(
+        "Error in consistency check of change %s", notes.getChangeId());
   }
 
   private Result result() {
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 6286a2f..65bef70 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
 
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -43,11 +44,9 @@
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class EmailReviewComments implements Runnable, RequestContext {
-  private static final Logger log = LoggerFactory.getLogger(EmailReviewComments.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     // TODO(dborowitz/wyatta): Rationalize these arguments so HTML and text templates are operating
@@ -150,7 +149,7 @@
       cm.setAccountsToNotify(accountsToNotify);
       cm.send();
     } catch (Exception e) {
-      log.error("Cannot email comments for " + patchSet.getId(), e);
+      logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.getId());
     } finally {
       requestContext.setContext(old);
       if (db != null) {
diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java
index 658c91c..c577f2d 100644
--- a/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -34,13 +35,10 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Resolve in which tags and branches a commit is included. */
 public class IncludedInResolver {
-
-  private static final Logger log = LoggerFactory.getLogger(IncludedInResolver.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static Result resolve(Repository repo, RevWalk rw, RevCommit commit) throws IOException {
     RevFlag flag = newFlag(rw);
@@ -206,13 +204,9 @@
       } catch (MissingObjectException notHere) {
         // Log the problem with this branch, but keep processing.
         //
-        log.warn(
-            "Reference "
-                + ref.getName()
-                + " in "
-                + repo.getDirectory()
-                + " points to dangling object "
-                + ref.getObjectId());
+        logger.atWarning().log(
+            "Reference %s in %s points to dangling object %s",
+            ref.getName(), repo.getDirectory(), ref.getObjectId());
         continue;
       }
       commitToRef.put(commit, ref.getName());
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index a192228..74810e9 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -22,6 +22,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -29,6 +30,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -37,23 +39,18 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class MergeabilityCacheImpl implements MergeabilityCache {
-  private static final Logger log = LoggerFactory.getLogger(MergeabilityCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String CACHE_NAME = "mergeability";
 
@@ -145,33 +142,24 @@
 
       @Override
       public byte[] serialize(EntryKey object) {
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        MergeabilityKeyProto.Builder b = MergeabilityKeyProto.newBuilder();
-        object.getCommit().copyRawTo(buf, 0);
-        b.setCommit(ByteString.copyFrom(buf));
-        object.getInto().copyRawTo(buf, 0);
-        b.setInto(ByteString.copyFrom(buf));
-        b.setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(object.getSubmitType()));
-        b.setMergeStrategy(object.getMergeStrategy());
-        return ProtoCacheSerializers.toByteArray(b.build());
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
+        return ProtoCacheSerializers.toByteArray(
+            MergeabilityKeyProto.newBuilder()
+                .setCommit(idConverter.toByteString(object.getCommit()))
+                .setInto(idConverter.toByteString(object.getInto()))
+                .setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(object.getSubmitType()))
+                .setMergeStrategy(object.getMergeStrategy())
+                .build());
       }
 
       @Override
       public EntryKey deserialize(byte[] in) {
-        MergeabilityKeyProto proto;
-        try {
-          proto = MergeabilityKeyProto.parseFrom(in);
-        } catch (IOException e) {
-          throw new IllegalArgumentException("Failed to deserialize mergeability cache key");
-        }
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        proto.getCommit().copyTo(buf, 0);
-        ObjectId commit = ObjectId.fromRaw(buf);
-        proto.getInto().copyTo(buf, 0);
-        ObjectId into = ObjectId.fromRaw(buf);
+        MergeabilityKeyProto proto =
+            ProtoCacheSerializers.parseUnchecked(MergeabilityKeyProto.parser(), in);
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
         return new EntryKey(
-            commit,
-            into,
+            idConverter.fromByteString(proto.getCommit()),
+            idConverter.fromByteString(proto.getInto()),
             SUBMIT_TYPE_CONVERTER.convert(proto.getSubmitType()),
             proto.getMergeStrategy());
       }
@@ -224,11 +212,9 @@
             }
           });
     } catch (ExecutionException | UncheckedExecutionException e) {
-      log.error(
-          String.format(
-              "Error checking mergeability of %s into %s (%s)",
-              key.commit.name(), key.into.name(), key.submitType.name()),
-          e.getCause());
+      logger.atSevere().withCause(e.getCause()).log(
+          "Error checking mergeability of %s into %s (%s)",
+          key.commit.name(), key.into.name(), key.submitType.name());
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index d05d133..b979240 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -63,11 +64,9 @@
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class PatchSetInserter implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(PatchSetInserter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     PatchSetInserter create(ChangeNotes notes, PatchSet.Id psId, ObjectId commitId);
@@ -301,7 +300,8 @@
         cm.setAccountsToNotify(accountsToNotify);
         cm.send();
       } catch (Exception err) {
-        log.error("Cannot send email for new patch set on change " + change.getId(), err);
+        logger.atSevere().withCause(err).log(
+            "Cannot send email for new patch set on change %s", change.getId());
       }
     }
 
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index bfb1692..22f98b8 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -38,12 +39,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Utility methods related to rebasing changes. */
 public class RebaseUtil {
-  private static final Logger log = LoggerFactory.getLogger(RebaseUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeNotes.Factory notesFactory;
@@ -69,10 +68,8 @@
     } catch (RestApiException e) {
       return false;
     } catch (OrmException | IOException e) {
-      log.warn(
-          String.format(
-              "Error checking if patch set %s on %s can be rebased", patchSet.getId(), dest),
-          e);
+      logger.atWarning().withCause(e).log(
+          "Error checking if patch set %s on %s can be rebased", patchSet.getId(), dest);
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index 74ca54b..e2258c0 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -35,11 +36,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SetAssigneeOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(SetAssigneeOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     SetAssigneeOp create(IdentifiedUser assignee);
@@ -127,7 +126,8 @@
       cm.setFrom(user.get().getAccountId());
       cm.send();
     } catch (Exception err) {
-      log.error("Cannot send email to new assignee of change " + change.getId(), err);
+      logger.atSevere().withCause(err).log(
+          "Cannot send email to new assignee of change %s", change.getId());
     }
     assigneeChanged.fire(
         change, ctx.getAccount(), oldAssignee != null ? oldAssignee.state() : null, ctx.getWhen());
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index 509f774..cff1ac7 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -46,8 +47,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Helper to sort {@link ChangeData}s based on {@link RevWalk} ordering.
@@ -63,7 +62,7 @@
  * of the changes was updated.
  */
 public class WalkSorter {
-  private static final Logger log = LoggerFactory.getLogger(WalkSorter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final Ordering<List<PatchSetData>> PROJECT_LIST_SORTER =
       Ordering.natural()
@@ -237,7 +236,8 @@
         RevCommit c = rw.parseCommit(id);
         byCommit.put(c, PatchSetData.create(cd, maxPs, c));
       } catch (MissingObjectException | IncorrectObjectTypeException e) {
-        log.warn("missing commit " + id.name() + " for patch set " + maxPs.getId(), e);
+        logger.atWarning().withCause(e).log(
+            "missing commit %s for patch set %s", id.name(), maxPs.getId());
       }
     }
     return byCommit;
diff --git a/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java b/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
index 29f288a..d6e61c4 100644
--- a/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
+++ b/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.server.account.GroupBackend;
@@ -25,11 +26,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Loads {@link AdministrateServerGroups} from {@code gerrit.config}. */
 public class AdministrateServerGroupsProvider implements Provider<ImmutableSet<GroupReference>> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final ImmutableSet<GroupReference> groups;
 
   @Inject
@@ -48,8 +49,7 @@
         if (g != null) {
           builder.add(g);
         } else {
-          Logger log = LoggerFactory.getLogger(getClass());
-          log.warn("Group \"{}\" not available, skipping.", name);
+          logger.atWarning().log("Group \"%s\" not available, skipping.", name);
         }
       }
       groups = builder.build();
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index cb0cdf9..57255a3 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -76,7 +76,6 @@
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
@@ -262,7 +261,6 @@
     factory(MergedSender.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
-    factory(PluginUser.Factory.class);
     factory(ProjectState.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
diff --git a/java/com/google/gerrit/server/config/GerritRuntime.java b/java/com/google/gerrit/server/config/GerritRuntime.java
new file mode 100644
index 0000000..ac4cede
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritRuntime.java
@@ -0,0 +1,24 @@
+// 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.config;
+
+/** Represents the current runtime environment in which Gerrit is running. */
+public enum GerritRuntime {
+  /** Gerrit is running as a server, with all its features. */
+  DAEMON,
+
+  /** Gerrit is running from the command line, in batch mode (reindex, ...). */
+  BATCH
+}
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index e02bf1c..8df21da 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.joining;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.securestore.SecureStore;
@@ -31,8 +32,6 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Provides {@link Config} annotated with {@link GerritServerConfig}.
@@ -44,7 +43,7 @@
  */
 @Singleton
 public class GerritServerConfigProvider implements Provider<Config> {
-  private static final Logger log = LoggerFactory.getLogger(GerritServerConfigProvider.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final SitePaths site;
   private final SecureStore secureStore;
@@ -78,7 +77,7 @@
   public GerritConfig loadConfig() {
     FileBasedConfig baseConfig = loadConfig(null, site.gerrit_config);
     if (!baseConfig.getFile().exists()) {
-      log.info("No " + site.gerrit_config.toAbsolutePath() + "; assuming defaults");
+      logger.atInfo().log("No %s; assuming defaults", site.gerrit_config.toAbsolutePath());
     }
 
     FileBasedConfig noteDbConfigOverBaseConfig = loadConfig(baseConfig, site.notedb_config);
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
index 61adadd..1890de8 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Issues a configuration reload from the GerritServerConfigProvider and notify all listeners. */
 @Singleton
 public class GerritServerConfigReloader {
-  private static final Logger log = LoggerFactory.getLogger(GerritServerConfigReloader.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GerritServerConfigProvider configProvider;
   private final DynamicSet<GerritConfigListener> configListeners;
@@ -42,9 +41,9 @@
    * reload is fully completed before a new one starts.
    */
   public List<ConfigUpdatedEvent.Update> reloadConfig() {
-    log.info("Starting server configuration reload");
+    logger.atInfo().log("Starting server configuration reload");
     List<ConfigUpdatedEvent.Update> updates = fireUpdatedConfigEvent(configProvider.updateConfig());
-    log.info("Server configuration reload completed succesfully");
+    logger.atInfo().log("Server configuration reload completed succesfully");
     return updates;
   }
 
diff --git a/java/com/google/gerrit/server/config/GitwebCgiConfig.java b/java/com/google/gerrit/server/config/GitwebCgiConfig.java
index 153cddc..d7fb83c 100644
--- a/java/com/google/gerrit/server/config/GitwebCgiConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebCgiConfig.java
@@ -17,17 +17,16 @@
 import static java.nio.file.Files.isExecutable;
 import static java.nio.file.Files.isRegularFile;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class GitwebCgiConfig {
-  private static final Logger log = LoggerFactory.getLogger(GitwebCgiConfig.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public GitwebCgiConfig disabled() {
     return new GitwebCgiConfig();
@@ -84,11 +83,11 @@
     } else if (isRegularFile(pkgCgi) && isExecutable(pkgCgi)) {
       // Use the OS packaged CGI.
       //
-      log.debug("Assuming gitweb at " + pkgCgi);
+      logger.atFine().log("Assuming gitweb at %s", pkgCgi);
       cgi = pkgCgi;
 
     } else {
-      log.warn("gitweb not installed (no " + pkgCgi + " found)");
+      logger.atWarning().log("gitweb not installed (no %s found)", pkgCgi);
       cgi = null;
       resourcePaths = new String[] {};
     }
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index 6c0f769..f38572d 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -19,6 +19,7 @@
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static com.google.common.base.Strings.nullToEmpty;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GitwebType;
 import com.google.gerrit.common.data.ParameterizedString;
@@ -36,11 +37,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class GitwebConfig {
-  private static final Logger log = LoggerFactory.getLogger(GitwebConfig.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static boolean isDisabled(Config cfg) {
     return isEmptyString(cfg, "gitweb", null, "url")
@@ -124,10 +123,10 @@
         if (isValidPathSeparator(c)) {
           type.setPathSeparator(firstNonNull(c, defaultType.getPathSeparator()));
         } else {
-          log.warn("Invalid gitweb.pathSeparator: " + c);
+          logger.atWarning().log("Invalid gitweb.pathSeparator: %s", c);
         }
       } else {
-        log.warn("gitweb.pathSeparator is not a single character: " + pathSeparator);
+        logger.atWarning().log("gitweb.pathSeparator is not a single character: %s", pathSeparator);
       }
     }
     return type;
diff --git a/java/com/google/gerrit/server/config/GroupSetProvider.java b/java/com/google/gerrit/server/config/GroupSetProvider.java
index a8c1674..2255a67 100644
--- a/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupBackend;
@@ -25,11 +26,10 @@
 import com.google.inject.Provider;
 import java.util.List;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Parses groups referenced in the {@code gerrit.config} file. */
 public abstract class GroupSetProvider implements Provider<Set<AccountGroup.UUID>> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected Set<AccountGroup.UUID> groupIds;
 
@@ -46,8 +46,7 @@
         if (g != null) {
           builder.add(g.getUUID());
         } else {
-          Logger log = LoggerFactory.getLogger(getClass());
-          log.warn("Group \"{}\" not available, skipping.", n);
+          logger.atWarning().log("Group \"%s\" not available, skipping.", n);
         }
       }
       groupIds = builder.build();
diff --git a/java/com/google/gerrit/server/config/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index a46efb8..69b300d 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
@@ -35,12 +36,11 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class PluginConfigFactory implements ReloadPluginListener {
-  private static final Logger log = LoggerFactory.getLogger(PluginConfigFactory.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String EXTENSION = ".config";
 
   private final SitePaths site;
@@ -222,7 +222,7 @@
     GlobalPluginConfig pluginConfig = new GlobalPluginConfig(pluginName, cfg, secureStore);
     pluginConfigs.put(pluginName, pluginConfig);
     if (!cfg.getFile().exists()) {
-      log.info("No " + pluginConfigFile.toAbsolutePath() + "; assuming defaults");
+      logger.atInfo().log("No %s; assuming defaults", pluginConfigFile.toAbsolutePath());
       return pluginConfig;
     }
 
@@ -230,9 +230,9 @@
       cfg.load();
     } catch (ConfigInvalidException e) {
       // This is an error in user input, don't spam logs with a stack trace.
-      log.warn("Failed to load " + pluginConfigFile.toAbsolutePath() + ": " + e);
+      logger.atWarning().log("Failed to load %s: %s", pluginConfigFile.toAbsolutePath(), e);
     } catch (IOException e) {
-      log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
+      logger.atWarning().withCause(e).log("Failed to load %s", pluginConfigFile.toAbsolutePath());
     }
 
     return pluginConfig;
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 4a72ffc..d30e080 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
@@ -36,8 +37,6 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @ExtensionPoint
 public class ProjectConfigEntry {
@@ -299,7 +298,7 @@
   public void onUpdate(Project.NameKey project, Long oldValue, Long newValue) {}
 
   public static class UpdateChecker implements GitReferenceUpdatedListener {
-    private static final Logger log = LoggerFactory.getLogger(UpdateChecker.class);
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
     private final GitRepositoryManager repoManager;
     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
@@ -350,9 +349,8 @@
           }
         }
       } catch (IOException | ConfigInvalidException e) {
-        log.error(
-            String.format("Failed to check if plugin config of project %s was updated.", p.get()),
-            e);
+        logger.atSevere().withCause(e).log(
+            "Failed to check if plugin config of project %s was updated.", p.get());
       }
     }
 
diff --git a/java/com/google/gerrit/server/config/ScheduleConfig.java b/java/com/google/gerrit/server/config/ScheduleConfig.java
index 9fff101..d62e7a2 100644
--- a/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -20,6 +20,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import java.time.DayOfWeek;
 import java.time.Duration;
@@ -32,8 +33,6 @@
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * This class reads a schedule for running a periodic background job from a Git config.
@@ -101,7 +100,7 @@
  */
 @AutoValue
 public abstract class ScheduleConfig {
-  private static final Logger log = LoggerFactory.getLogger(ScheduleConfig.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @VisibleForTesting static final String KEY_INTERVAL = "interval";
   @VisibleForTesting static final String KEY_STARTTIME = "startTime";
@@ -157,28 +156,26 @@
   private boolean isInvalidOrMissing(long interval, long initialDelay) {
     String key = section() + (subsection() != null ? "." + subsection() : "");
     if (interval == MISSING_CONFIG && initialDelay == MISSING_CONFIG) {
-      log.info("No schedule configuration for \"{}\".", key);
+      logger.atInfo().log("No schedule configuration for \"%s\".", key);
       return true;
     }
 
     if (interval == MISSING_CONFIG) {
-      log.error(
-          "Incomplete schedule configuration for \"{}\" is ignored. Missing value for \"{}\".",
-          key,
-          key + "." + keyInterval());
+      logger.atSevere().log(
+          "Incomplete schedule configuration for \"%s\" is ignored. Missing value for \"%s\".",
+          key, key + "." + keyInterval());
       return true;
     }
 
     if (initialDelay == MISSING_CONFIG) {
-      log.error(
-          "Incomplete schedule configuration for \"{}\" is ignored. Missing value for \"{}\".",
-          key,
-          key + "." + keyStartTime());
+      logger.atSevere().log(
+          "Incomplete schedule configuration for \"%s\" is ignored. Missing value for \"%s\".",
+          key, key + "." + keyStartTime());
       return true;
     }
 
     if (interval <= 0 || initialDelay < 0) {
-      log.error("Invalid schedule configuration for \"{}\" is ignored. ", key);
+      logger.atSevere().log("Invalid schedule configuration for \"%s\" is ignored. ", key);
       return true;
     }
 
diff --git a/java/com/google/gerrit/server/config/SysExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
index b9fe34c..2e97c06 100644
--- a/java/com/google/gerrit/server/config/SysExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -47,7 +47,7 @@
     int poolSize =
         config.getInt(
             "receive", null, "threadPoolSize", Runtime.getRuntime().availableProcessors());
-    return queues.createQueue(poolSize, "ReceiveCommits");
+    return queues.createQueue(poolSize, "ReceiveCommits", true);
   }
 
   @Provides
@@ -59,7 +59,7 @@
     if (poolSize == 0) {
       return MoreExecutors.newDirectExecutorService();
     }
-    return queues.createQueue(poolSize, "SendEmail");
+    return queues.createQueue(poolSize, "SendEmail", true);
   }
 
   @Provides
diff --git a/java/com/google/gerrit/server/config/TrackingFootersProvider.java b/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index 7b23fcc..ff1910d 100644
--- a/java/com/google/gerrit/server/config/TrackingFootersProvider.java
+++ b/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -26,18 +27,17 @@
 import java.util.Set;
 import java.util.regex.PatternSyntaxException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Provides a list of all configured {@link TrackingFooter}s. */
 @Singleton
 public class TrackingFootersProvider implements Provider<TrackingFooters> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static String TRACKING_ID_TAG = "trackingid";
   private static String FOOTER_TAG = "footer";
   private static String SYSTEM_TAG = "system";
   private static String REGEX_TAG = "match";
   private final List<TrackingFooter> trackingFooters = new ArrayList<>();
-  private static final Logger log = LoggerFactory.getLogger(TrackingFootersProvider.class);
 
   @Inject
   TrackingFootersProvider(@GerritServerConfig Config cfg) {
@@ -50,36 +50,27 @@
 
       if (footers.isEmpty()) {
         configValid = false;
-        log.error(
-            "Missing " + TRACKING_ID_TAG + "." + name + "." + FOOTER_TAG + " in gerrit.config");
+        logger.atSevere().log(
+            "Missing %s.%s.%s in gerrit.config", TRACKING_ID_TAG, name, FOOTER_TAG);
       }
 
       String system = cfg.getString(TRACKING_ID_TAG, name, SYSTEM_TAG);
       if (system == null || system.isEmpty()) {
         configValid = false;
-        log.error(
-            "Missing " + TRACKING_ID_TAG + "." + name + "." + SYSTEM_TAG + " in gerrit.config");
+        logger.atSevere().log(
+            "Missing %s.%s.%s in gerrit.config", TRACKING_ID_TAG, name, SYSTEM_TAG);
       } else if (system.length() > TrackingId.TRACKING_SYSTEM_MAX_CHAR) {
         configValid = false;
-        log.error(
-            "String too long \""
-                + system
-                + "\" in gerrit.config "
-                + TRACKING_ID_TAG
-                + "."
-                + name
-                + "."
-                + SYSTEM_TAG
-                + " (max "
-                + TrackingId.TRACKING_SYSTEM_MAX_CHAR
-                + " char)");
+        logger.atSevere().log(
+            "String too long \"%s\" in gerrit.config %s.%s.%s (max %d char)",
+            system, TRACKING_ID_TAG, name, SYSTEM_TAG, TrackingId.TRACKING_SYSTEM_MAX_CHAR);
       }
 
       String match = cfg.getString(TRACKING_ID_TAG, name, REGEX_TAG);
       if (match == null || match.isEmpty()) {
         configValid = false;
-        log.error(
-            "Missing " + TRACKING_ID_TAG + "." + name + "." + REGEX_TAG + " in gerrit.config");
+        logger.atSevere().log(
+            "Missing %s.%s.%s in gerrit.config", TRACKING_ID_TAG, name, REGEX_TAG);
       }
 
       if (configValid) {
@@ -88,17 +79,9 @@
             trackingFooters.add(new TrackingFooter(footer, match, system));
           }
         } catch (PatternSyntaxException e) {
-          log.error(
-              "Invalid pattern \""
-                  + match
-                  + "\" in gerrit.config "
-                  + TRACKING_ID_TAG
-                  + "."
-                  + name
-                  + "."
-                  + REGEX_TAG
-                  + ": "
-                  + e.getMessage());
+          logger.atSevere().log(
+              "Invalid pattern \"%s\" in gerrit.config %s.%s.%s: %s",
+              match, TRACKING_ID_TAG, name, REGEX_TAG, e.getMessage());
         }
       }
     }
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index 68d2a34..a7f9a05 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -20,6 +20,7 @@
 import static org.pegdown.Extensions.SUPPRESS_ALL_HTML;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -37,11 +38,9 @@
 import org.pegdown.ast.Node;
 import org.pegdown.ast.RootNode;
 import org.pegdown.ast.TextNode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class MarkdownFormatter {
-  private static final Logger log = LoggerFactory.getLogger(MarkdownFormatter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String defaultCss;
 
@@ -51,7 +50,7 @@
     try {
       src = readPegdownCss(file);
     } catch (IOException err) {
-      log.warn("Cannot load pegdown.css", err);
+      logger.atWarning().withCause(err).log("Cannot load pegdown.css");
       src = "";
     }
     defaultCss = file.get() ? null : src;
@@ -64,7 +63,7 @@
     try {
       return readPegdownCss(new AtomicBoolean());
     } catch (IOException err) {
-      log.warn("Cannot load pegdown.css", err);
+      logger.atWarning().withCause(err).log("Cannot load pegdown.css");
       return "";
     }
   }
diff --git a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index eef6d35..c606919 100644
--- a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -36,12 +37,10 @@
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.store.IndexOutput;
 import org.apache.lucene.store.RAMDirectory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class QueryDocumentationExecutor {
-  private static final Logger log = LoggerFactory.getLogger(QueryDocumentationExecutor.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static Map<String, Float> WEIGHTS =
       ImmutableMap.of(
@@ -70,7 +69,7 @@
       searcher = new IndexSearcher(reader);
       parser = new SimpleQueryParser(new StandardAnalyzer(), WEIGHTS);
     } catch (IOException e) {
-      log.error("Cannot initialize documentation full text index", e);
+      logger.atSevere().withCause(e).log("Cannot initialize documentation full text index");
       searcher = null;
       parser = null;
     }
@@ -107,7 +106,7 @@
     byte[] buffer = new byte[4096];
     InputStream index = getClass().getResourceAsStream(Constants.INDEX_ZIP);
     if (index == null) {
-      log.warn("No index available");
+      logger.atWarning().log("No index available");
       return null;
     }
 
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index 3d75e6a..a163b58 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -18,6 +18,7 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.restapi.RawInput;
 import java.io.IOException;
@@ -32,13 +33,10 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** A {@code TreeModification} which changes the content of a file. */
 public class ChangeFileContentModification implements TreeModification {
-
-  private static final Logger log = LoggerFactory.getLogger(ChangeFileContentModification.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final String filePath;
   private final RawInput newContent;
@@ -97,9 +95,9 @@
       } catch (IOException e) {
         String message =
             String.format("Could not change the content of %s", dirCacheEntry.getPathString());
-        log.error(message, e);
+        logger.atSevere().withCause(e).log(message);
       } catch (InvalidObjectIdException e) {
-        log.error("Invalid object id in submodule link", e);
+        logger.atSevere().withCause(e).log("Invalid object id in submodule link");
       }
     }
 
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 62e8d12..7d35070 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,13 +38,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Distributes Events to listeners if they are allowed to see them */
 @Singleton
 public class EventBroker implements EventDispatcher {
-  private static final Logger log = LoggerFactory.getLogger(EventBroker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends LifecycleModule {
     @Override
@@ -203,7 +202,8 @@
                   .getChange();
           return isVisibleTo(change, user);
         } catch (NoSuchChangeException e) {
-          log.debug("Change {} cannot be found, falling back on ref visibility check", cid.id);
+          logger.atFine().log(
+              "Change %s cannot be found, falling back on ref visibility check", cid.id);
         }
       }
       return isVisibleTo(refEvent.getBranchNameKey(), user);
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index aca0bd4..f675dd5 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -80,12 +81,10 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class EventFactory {
-  private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AccountCache accountCache;
   private final Emails emails;
@@ -135,7 +134,7 @@
     try (ReviewDb db = schema.open()) {
       return asChangeAttribute(db, change);
     } catch (OrmException e) {
-      log.error("Cannot open database connection", e);
+      logger.atSevere().withCause(e).log("Cannot open database connection");
       return new ChangeAttribute();
     }
   }
@@ -158,7 +157,8 @@
     try {
       a.commitMessage = changeDataFactory.create(db, change).commitMessage();
     } catch (Exception e) {
-      log.error("Error while getting full commit message for change " + a.number, e);
+      logger.atSevere().withCause(e).log(
+          "Error while getting full commit message for change %d", a.number);
     }
     a.url = getChangeUrl(change);
     a.owner = asAccountAttribute(change.getOwner());
@@ -450,9 +450,9 @@
         patchSetAttribute.files.add(p);
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Cannot get patch list: " + e.getMessage());
+      logger.atWarning().log("Cannot get patch list: %s", e.getMessage());
     } catch (PatchListNotAvailableException e) {
-      log.error("Cannot get patch list", e);
+      logger.atSevere().withCause(e).log("Cannot get patch list");
     }
   }
 
@@ -476,7 +476,7 @@
     try (ReviewDb db = schema.open()) {
       return asPatchSetAttribute(db, revWalk, change, patchSet);
     } catch (OrmException e) {
-      log.error("Cannot open database connection", e);
+      logger.atSevere().withCause(e).log("Cannot open database connection");
       return new PatchSetAttribute();
     }
   }
@@ -523,11 +523,11 @@
       }
       p.kind = changeKindCache.getChangeKind(db, change, patchSet);
     } catch (IOException | OrmException e) {
-      log.error("Cannot load patch set data for " + patchSet.getId(), e);
+      logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.getId());
     } catch (PatchListObjectTooLargeException e) {
-      log.warn(String.format("Cannot get size information for %s: %s", pId, e.getMessage()));
+      logger.atWarning().log("Cannot get size information for %s: %s", pId, e.getMessage());
     } catch (PatchListNotAvailableException e) {
-      log.error(String.format("Cannot get size information for %s.", pId), e);
+      logger.atSevere().withCause(e).log("Cannot get size information for %s.", pId);
     }
     return p;
   }
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index a63e1f8..9592238 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -69,8 +70,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class StreamEventsApiListener
@@ -89,7 +88,7 @@
         RevisionCreatedListener,
         TopicEditedListener,
         VoteDeletedListener {
-  private static final Logger log = LoggerFactory.getLogger(StreamEventsApiListener.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends AbstractModule {
     @Override
@@ -267,7 +266,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -283,7 +282,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -301,7 +300,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -321,7 +320,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -339,7 +338,7 @@
         dispatcher.get().postEvent(change, event);
       }
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -366,7 +365,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -391,7 +390,7 @@
     try {
       dispatcher.get().postEvent(refName, event);
     } catch (PermissionBackendException e) {
-      log.error("error while posting event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -411,7 +410,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -429,7 +428,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -447,7 +446,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -465,7 +464,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -480,7 +479,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -495,7 +494,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 
@@ -515,7 +514,7 @@
 
       dispatcher.get().postEvent(change, event);
     } catch (OrmException | PermissionBackendException e) {
-      log.error("Failed to dispatch event", e);
+      logger.atSevere().withCause(e).log("Failed to dispatch event");
     }
   }
 }
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index 3aeaf43..7320fd3 100644
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -25,12 +26,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AssigneeChanged {
-  private static final Logger log = LoggerFactory.getLogger(AssigneeChanged.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<AssigneeChangedListener> listeners;
   private final EventUtil util;
@@ -61,7 +60,7 @@
         }
       }
     } catch (OrmException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index d600240..3a19e97 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -32,12 +33,10 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ChangeAbandoned {
-  private static final Logger log = LoggerFactory.getLogger(ChangeAbandoned.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ChangeAbandonedListener> listeners;
   private final EventUtil util;
@@ -75,13 +74,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 04dba5c..5b882b8 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -32,12 +33,10 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ChangeMerged {
-  private static final Logger log = LoggerFactory.getLogger(ChangeMerged.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ChangeMergedListener> listeners;
   private final EventUtil util;
@@ -69,13 +68,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index deee7e0..d62b6c1 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -32,12 +33,10 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ChangeRestored {
-  private static final Logger log = LoggerFactory.getLogger(ChangeRestored.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ChangeRestoredListener> listeners;
   private final EventUtil util;
@@ -69,13 +68,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index 1e91ab3..5f8f8c3 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.ChangeRevertedListener;
@@ -23,12 +24,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ChangeReverted {
-  private static final Logger log = LoggerFactory.getLogger(ChangeReverted.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ChangeRevertedListener> listeners;
   private final EventUtil util;
@@ -53,7 +52,7 @@
         }
       }
     } catch (OrmException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index ec35ea8..8ba9f82 100644
--- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -34,12 +35,10 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class CommentAdded {
-  private static final Logger log = LoggerFactory.getLogger(CommentAdded.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<CommentAddedListener> listeners;
   private final EventUtil util;
@@ -79,13 +78,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index b0bbce4..74fba9a 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -41,12 +42,10 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class EventUtil {
-  private static final Logger log = LoggerFactory.getLogger(EventUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final ImmutableSet<ListChangesOption> CHANGE_OPTIONS;
 
@@ -121,26 +120,17 @@
   }
 
   public void logEventListenerError(Object event, Object listener, Exception error) {
-    if (log.isDebugEnabled()) {
-      log.debug(
-          String.format(
-              "Error in event listener %s for event %s",
-              listener.getClass().getName(), event.getClass().getName()),
-          error);
-    } else {
-      log.warn(
-          "Error in event listener {} for event {}: {}",
-          listener.getClass().getName(),
-          event.getClass().getName(),
-          error.getMessage());
-    }
+    logger.atWarning().log(
+        "Error in event listener %s for event %s: %s",
+        listener.getClass().getName(), event.getClass().getName(), error.getMessage());
+    logger.atFine().withCause(error).log(
+        "Cause of error in event listener %s:", listener.getClass().getName());
   }
 
   public static void logEventListenerError(Object listener, Exception error) {
-    if (log.isDebugEnabled()) {
-      log.debug(String.format("Error in event listener %s", listener.getClass().getName()), error);
-    } else {
-      log.warn("Error in event listener {}: {}", listener.getClass().getName(), error.getMessage());
-    }
+    logger.atWarning().log(
+        "Error in event listener %s: %s", listener.getClass().getName(), error.getMessage());
+    logger.atFine().withCause(error).log(
+        "Cause of error in event listener %s", listener.getClass().getName());
   }
 }
diff --git a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index 948ff2d..9a0247a 100644
--- a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -28,12 +29,10 @@
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class HashtagsEdited {
-  private static final Logger log = LoggerFactory.getLogger(HashtagsEdited.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<HashtagsEditedListener> listeners;
   private final EventUtil util;
@@ -66,7 +65,7 @@
         }
       }
     } catch (OrmException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index b30d473..acd275d 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -25,12 +26,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class PrivateStateChanged {
-  private static final Logger log = LoggerFactory.getLogger(PrivateStateChanged.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<PrivateStateChangedListener> listeners;
   private final EventUtil util;
@@ -55,7 +54,7 @@
         }
       }
     } catch (OrmException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index 433ae06..e33715b 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -34,12 +35,10 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ReviewerAdded {
-  private static final Logger log = LoggerFactory.getLogger(ReviewerAdded.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ReviewerAddedListener> listeners;
   private final EventUtil util;
@@ -76,13 +75,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 0037783..011a3e8 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -34,12 +35,10 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ReviewerDeleted {
-  private static final Logger log = LoggerFactory.getLogger(ReviewerDeleted.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ReviewerDeletedListener> listeners;
   private final EventUtil util;
@@ -83,13 +82,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index e4fa647..f203f5d 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -32,12 +33,10 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class RevisionCreated {
-  private static final Logger log = LoggerFactory.getLogger(RevisionCreated.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final RevisionCreated DISABLED =
       new RevisionCreated() {
@@ -89,13 +88,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 354137a..45962f9 100644
--- a/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -25,12 +26,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class TopicEdited {
-  private static final Logger log = LoggerFactory.getLogger(TopicEdited.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<TopicEditedListener> listeners;
   private final EventUtil util;
@@ -56,7 +55,7 @@
         }
       }
     } catch (OrmException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index bd48c32..5480dd8 100644
--- a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -34,12 +35,10 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class VoteDeleted {
-  private static final Logger log = LoggerFactory.getLogger(VoteDeleted.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<VoteDeletedListener> listeners;
   private final EventUtil util;
@@ -83,13 +82,13 @@
         }
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Couldn't fire event: " + e.getMessage());
+      logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
         | OrmException
         | PermissionBackendException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index 62312d8..3f9f35b 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -25,12 +26,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class WorkInProgressStateChanged {
-  private static final Logger log = LoggerFactory.getLogger(WorkInProgressStateChanged.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<WorkInProgressStateChangedListener> listeners;
   private final EventUtil util;
@@ -56,7 +55,7 @@
         }
       }
     } catch (OrmException e) {
-      log.error("Couldn't fire event", e);
+      logger.atSevere().withCause(e).log("Couldn't fire event");
     }
   }
 
diff --git a/java/com/google/gerrit/server/extensions/webui/UiActions.java b/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 26a7957..f8cb4ce 100644
--- a/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -21,6 +21,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
@@ -47,12 +48,10 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class UiActions {
-  private static final Logger log = LoggerFactory.getLogger(UiActions.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static Predicate<UiAction.Description> enabled() {
     return UiAction.Description::isEnabled;
@@ -131,8 +130,8 @@
     try {
       view = e.getProvider().get();
     } catch (RuntimeException err) {
-      log.error(
-          String.format("error creating view %s.%s", e.getPluginName(), e.getExportName()), err);
+      logger.atSevere().withCause(err).log(
+          "error creating view %s.%s", e.getPluginName(), e.getExportName());
       return null;
     }
 
@@ -154,8 +153,8 @@
     try {
       globalRequired = GlobalPermission.fromAnnotation(e.getPluginName(), view.getClass());
     } catch (PermissionBackendException err) {
-      log.error(
-          String.format("exception testing view %s.%s", e.getPluginName(), e.getExportName()), err);
+      logger.atSevere().withCause(err).log(
+          "exception testing view %s.%s", e.getPluginName(), e.getExportName());
       return null;
     }
     if (!globalRequired.isEmpty()) {
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 3bf89c7..3624695 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -35,14 +36,9 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.storage.pack.PackConfig;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class GarbageCollection {
-  private static final Logger log = LoggerFactory.getLogger(GarbageCollection.class);
-
-  public static final String LOG_NAME = "gc_log";
-  private static final Logger gcLog = LoggerFactory.getLogger(LOG_NAME);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final GarbageCollectionQueue gcQueue;
@@ -121,7 +117,7 @@
       try {
         l.onGarbageCollected(event);
       } catch (RuntimeException e) {
-        log.warn("Failure in GarbageCollectorListener", e);
+        logger.atWarning().withCause(e).log("Failure in GarbageCollectorListener");
       }
     }
   }
@@ -142,7 +138,7 @@
       }
       b.append(s);
     }
-    gcLog.info(b.toString());
+    logger.atInfo().log(b.toString());
   }
 
   private static void logGcConfiguration(
@@ -182,8 +178,7 @@
     print(writer, "failed.\n\n");
     StringBuilder b = new StringBuilder();
     b.append("[").append(projectName.get()).append("]");
-    gcLog.error(b.toString(), e);
-    log.error(b.toString(), e);
+    logger.atSevere().withCause(e).log(b.toString());
   }
 
   private static void print(PrintWriter writer, String message) {
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java b/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
index e03ef67..b711708 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.flogger.backend.Platform;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -26,6 +27,8 @@
 import org.eclipse.jgit.lib.Config;
 
 public class GarbageCollectionLogFile implements LifecycleListener {
+  private static final String LOG_NAME = "gc_log";
+
   @Inject
   public GarbageCollectionLogFile(SitePaths sitePaths, @GerritServerConfig Config config) {
     if (SystemLog.shouldConfigure()) {
@@ -38,15 +41,24 @@
 
   @Override
   public void stop() {
-    LogManager.getLogger(GarbageCollection.LOG_NAME).removeAllAppenders();
+    getLogger(GarbageCollection.class).removeAllAppenders();
+    getLogger(GarbageCollectionRunner.class).removeAllAppenders();
   }
 
   private static void initLogSystem(Path logdir, boolean rotate) {
-    Logger gcLogger = LogManager.getLogger(GarbageCollection.LOG_NAME);
+    initGcLogger(logdir, rotate, getLogger(GarbageCollection.class));
+    initGcLogger(logdir, rotate, getLogger(GarbageCollectionRunner.class));
+  }
+
+  private static Logger getLogger(Class<?> clazz) {
+    return LogManager.getLogger(Platform.getBackend(clazz.getName()).getLoggerName());
+  }
+
+  private static void initGcLogger(Path logdir, boolean rotate, Logger gcLogger) {
     gcLogger.removeAllAppenders();
     gcLogger.addAppender(
         SystemLog.createAppender(
-            logdir, GarbageCollection.LOG_NAME, new PatternLayout("[%d] %-5p %x: %m%n"), rotate));
+            logdir, LOG_NAME, new PatternLayout("[%d] %-5p %x: %m%n"), rotate));
     gcLogger.setAdditivity(false);
   }
 }
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
index e4316c5..b44251d 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
@@ -15,16 +15,15 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.GcConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Runnable to enable scheduling gc to run periodically */
 public class GarbageCollectionRunner implements Runnable {
-  private static final Logger gcLog = LoggerFactory.getLogger(GarbageCollection.LOG_NAME);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static class Lifecycle implements LifecycleListener {
     private final WorkQueue queue;
@@ -61,7 +60,7 @@
 
   @Override
   public void run() {
-    gcLog.info("Triggering gc on all repositories");
+    logger.atInfo().log("Triggering gc on all repositories");
     garbageCollectionFactory.create().run(Lists.newArrayList(projectCache.all()));
   }
 
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index 4a7c7e9..bb65fa8 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -44,8 +45,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Helper for assigning groups to commits during {@code ReceiveCommits}.
@@ -75,7 +74,7 @@
  * visited, call {@link #getGroups()} for the result.
  */
 public class GroupCollector {
-  private static final Logger log = LoggerFactory.getLogger(GroupCollector.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static List<String> getDefaultGroups(PatchSet ps) {
     return ImmutableList.of(ps.getRevision().get());
@@ -281,7 +280,7 @@
       return ObjectId.fromString(group);
     } catch (IllegalArgumentException e) {
       // Shouldn't happen; some sort of corruption or manual tinkering?
-      log.warn("group for commit {} is not a SHA-1: {}", forCommit.name(), group);
+      logger.atWarning().log("group for commit %s is not a SHA-1: %s", forCommit.name(), group);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 23f2526..abe3410 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project;
@@ -45,13 +46,11 @@
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
 import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Manages Git repositories stored on the local filesystem. */
 @Singleton
 public class LocalDiskRepositoryManager implements GitRepositoryManager {
-  private static final Logger log = LoggerFactory.getLogger(LocalDiskRepositoryManager.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends LifecycleModule {
     @Override
@@ -98,7 +97,7 @@
         } else {
           desc = String.format("%d", limit);
         }
-        log.info(String.format("Defaulting core.streamFileThreshold to %s", desc));
+        logger.atInfo().log("Defaulting core.streamFileThreshold to %s", desc);
         cfg.setStreamFileThreshold(limit);
       }
       cfg.install();
@@ -193,9 +192,8 @@
       //
       File metaConfigLog = new File(db.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
       if (!metaConfigLog.getParentFile().mkdirs() || !metaConfigLog.createNewFile()) {
-        log.error(
-            String.format(
-                "Failed to create ref log for %s in repository %s", RefNames.REFS_CONFIG, name));
+        logger.atSevere().log(
+            "Failed to create ref log for %s in repository %s", RefNames.REFS_CONFIG, name);
       }
 
       return db;
@@ -248,7 +246,8 @@
           Integer.MAX_VALUE,
           visitor);
     } catch (IOException e) {
-      log.error("Error walking repository tree " + visitor.startFolder.toAbsolutePath(), e);
+      logger.atSevere().withCause(e).log(
+          "Error walking repository tree %s", visitor.startFolder.toAbsolutePath());
     }
   }
 
@@ -288,7 +287,7 @@
 
     @Override
     public FileVisitResult visitFileFailed(Path file, IOException e) {
-      log.warn(e.getMessage());
+      logger.atWarning().log(e.getMessage());
       return FileVisitResult.CONTINUE;
     }
 
@@ -303,7 +302,7 @@
       Project.NameKey nameKey = getProjectName(startFolder, p);
       if (getBasePath(nameKey).equals(startFolder)) {
         if (isUnreasonableName(nameKey)) {
-          log.warn("Ignoring unreasonably named repository " + p.toAbsolutePath());
+          logger.atWarning().log("Ignoring unreasonably named repository %s", p.toAbsolutePath());
         } else {
           found.add(nameKey);
         }
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index e2bec34..637be24 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
@@ -92,8 +93,6 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Utility methods used during the merge process.
@@ -104,7 +103,7 @@
  * {@code BatchUpdate}.
  */
 public class MergeUtil {
-  private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static class PluggableCommitMessageGenerator {
     private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
@@ -466,7 +465,7 @@
     try {
       return approvalsUtil.byPatchSet(db.get(), notes, user, psId, null, null);
     } catch (OrmException e) {
-      log.error("Can't read approval records for " + psId, e);
+      logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
       return Collections.emptyList();
     }
   }
@@ -499,7 +498,7 @@
     try (ObjectInserter ins = new InMemoryInserter(repo)) {
       return newThreeWayMerger(ins, repo.getConfig()).merge(new AnyObjectId[] {mergeTip, toMerge});
     } catch (LargeObjectException e) {
-      log.warn("Cannot merge due to LargeObjectException: " + toMerge.name());
+      logger.atWarning().log("Cannot merge due to LargeObjectException: %s", toMerge.name());
       return false;
     } catch (NoMergeBaseException e) {
       return false;
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index bb50218..eced9c3 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.LabelId;
@@ -47,11 +48,9 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class MergedByPushOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(MergedByPushOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     MergedByPushOp create(
@@ -181,7 +180,8 @@
                       cm.setPatchSet(patchSet, info);
                       cm.send();
                     } catch (Exception e) {
-                      log.error("Cannot send email for submitted patch set " + psId, e);
+                      logger.atSevere().withCause(e).log(
+                          "Cannot send email for submitted patch set %s", psId);
                     }
                   }
 
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 2b9cad1..b72ea92 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -17,6 +17,7 @@
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
@@ -28,8 +29,6 @@
 import java.util.concurrent.TimeoutException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Progress reporting interface that multiplexes multiple sub-tasks.
@@ -48,7 +47,7 @@
  * multi-line progress messages would be impossible.)
  */
 public class MultiProgressMonitor {
-  private static final Logger log = LoggerFactory.getLogger(MultiProgressMonitor.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Constant indicating the total work units cannot be predicted. */
   public static final int UNKNOWN = 0;
@@ -215,10 +214,9 @@
                 String.format(
                     "(timeout %sms, cancelled)",
                     TimeUnit.MILLISECONDS.convert(now - deadline, NANOSECONDS));
-            log.warn(
-                String.format(
-                    "MultiProgressMonitor worker killed after %sms" + detailMessage, //
-                    TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS)));
+            logger.atWarning().log(
+                "MultiProgressMonitor worker killed after %sms: %s",
+                TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS), detailMessage);
           }
           break;
         }
@@ -232,7 +230,7 @@
         if (!done && workerFuture.isDone()) {
           // The worker may not have called end() explicitly, which is likely a
           // programming error.
-          log.warn("MultiProgressMonitor worker did not call end() before returning");
+          logger.atWarning().log("MultiProgressMonitor worker did not call end() before returning");
           end();
         }
       }
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 6ae8b91..1b83097 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -41,12 +42,11 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class SearchingChangeCacheImpl implements GitReferenceUpdatedListener {
-  private static final Logger log = LoggerFactory.getLogger(SearchingChangeCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   static final String ID_CACHE = "changes";
 
   public static class Module extends CacheModule {
@@ -120,7 +120,7 @@
       }
       return Collections.unmodifiableList(cds);
     } catch (ExecutionException e) {
-      log.warn("Cannot fetch changes for " + project, e);
+      logger.atWarning().withCause(e).log("Cannot fetch changes for %s", project);
       return Collections.emptyList();
     }
   }
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index 118223b..10b3411 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -17,6 +17,7 @@
 import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
 import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -38,11 +39,9 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class TagSet {
-  private static final Logger log = LoggerFactory.getLogger(TagSet.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Project.NameKey projectName;
   private final Map<String, CachedRef> refs;
@@ -141,7 +140,7 @@
         } catch (IOException err) {
           // Defer a cache update until later. No conclusion can be made
           // based on an exception reading from the repository storage.
-          log.warn("Error checking tags of " + projectName, err);
+          logger.atWarning().withCause(err).log("Error checking tags of %s", projectName);
         }
       }
     } finally {
@@ -185,7 +184,7 @@
         }
       }
     } catch (IOException e) {
-      log.warn("Error building tags for repository " + projectName, e);
+      logger.atWarning().withCause(e).log("Error building tags for repository %s", projectName);
     }
   }
 
@@ -302,7 +301,7 @@
       } catch (IncorrectObjectTypeException notCommit) {
         flags = new BitSet();
       } catch (IOException e) {
-        log.warn("Error on " + ref.getName() + " of " + projectName, e);
+        logger.atWarning().withCause(e).log("Error on %s of %s", ref.getName(), projectName);
         flags = new BitSet();
       }
       tags.add(new Tag(id, flags));
@@ -323,7 +322,7 @@
       // For instance, refs from refs/cache-automerge
       // will often end up here.
     } catch (IOException e) {
-      log.warn("Error on " + ref.getName() + " of " + projectName, e);
+      logger.atWarning().withCause(e).log("Error on %s of %s", ref.getName(), projectName);
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 29915ef5..46518e5 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.CaseFormat;
 import com.google.common.base.Supplier;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.Description;
@@ -48,12 +49,12 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Delayed execution of tasks using a background thread pool. */
 @Singleton
 public class WorkQueue {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static class Lifecycle implements LifecycleListener {
     private final WorkQueue workQueue;
 
@@ -79,31 +80,30 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(WorkQueue.class);
   private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION =
       new UncaughtExceptionHandler() {
         @Override
         public void uncaughtException(Thread t, Throwable e) {
-          log.error("WorkQueue thread " + t.getName() + " threw exception", e);
+          logger.atSevere().withCause(e).log("WorkQueue thread %s threw exception", t.getName());
         }
       };
 
-  private final MetricMaker metrics;
   private final ScheduledExecutorService defaultQueue;
   private final IdGenerator idGenerator;
+  private final MetricMaker metrics;
   private final CopyOnWriteArrayList<Executor> queues;
 
   @Inject
-  WorkQueue(MetricMaker metrics, IdGenerator idGenerator, @GerritServerConfig Config cfg) {
-    this(metrics, idGenerator, cfg.getInt("execution", "defaultThreadPoolSize", 1));
+  WorkQueue(IdGenerator idGenerator, @GerritServerConfig Config cfg, MetricMaker metrics) {
+    this(idGenerator, cfg.getInt("execution", "defaultThreadPoolSize", 1), metrics);
   }
 
   /** Constructor to allow binding the WorkQueue more explicitly in a vhost setup. */
-  public WorkQueue(MetricMaker metrics, IdGenerator idGenerator, int defaultThreadPoolSize) {
-    this.metrics = metrics;
+  public WorkQueue(IdGenerator idGenerator, int defaultThreadPoolSize, MetricMaker metrics) {
     this.idGenerator = idGenerator;
+    this.metrics = metrics;
     this.queues = new CopyOnWriteArrayList<>();
-    this.defaultQueue = createQueue(defaultThreadPoolSize, "WorkQueue");
+    this.defaultQueue = createQueue(defaultThreadPoolSize, "WorkQueue", true);
   }
 
   /** Get the default work queue, for miscellaneous tasks. */
@@ -111,13 +111,54 @@
     return defaultQueue;
   }
 
-  /** Create a new executor queue. */
-  public ScheduledExecutorService createQueue(int poolsize, String prefix) {
-    return createQueue(poolsize, prefix, Thread.NORM_PRIORITY);
+  /**
+   * Create a new executor queue.
+   *
+   * <p>Creates a new executor queue without associated metrics. This method is suitable for use by
+   * plugins.
+   *
+   * <p>If metrics are needed, use {@link #createQueue(int, String, int, boolean)} instead.
+   *
+   * @param poolsize the size of the pool.
+   * @param queueName the name of the queue.
+   */
+  public ScheduledThreadPoolExecutor createQueue(int poolsize, String queueName) {
+    return createQueue(poolsize, queueName, Thread.NORM_PRIORITY, false);
   }
 
-  public ScheduledThreadPoolExecutor createQueue(int poolsize, String prefix, int threadPriority) {
-    Executor executor = new Executor(poolsize, prefix);
+  /**
+   * Create a new executor queue, with default priority, optionally with metrics.
+   *
+   * <p>Creates a new executor queue, optionally with associated metrics. Metrics should not be
+   * requested for queues created by plugins.
+   *
+   * @param poolsize the size of the pool.
+   * @param queueName the name of the queue.
+   * @param withMetrics whether to create metrics.
+   */
+  public ScheduledThreadPoolExecutor createQueue(
+      int poolsize, String queueName, boolean withMetrics) {
+    return createQueue(poolsize, queueName, Thread.NORM_PRIORITY, withMetrics);
+  }
+
+  /**
+   * Create a new executor queue, optionally with metrics.
+   *
+   * <p>Creates a new executor queue, optionally with associated metrics. Metrics should not be
+   * requested for queues created by plugins.
+   *
+   * @param poolsize the size of the pool.
+   * @param queueName the name of the queue.
+   * @param threadPriority thread priority.
+   * @param withMetrics whether to create metrics.
+   */
+  public ScheduledThreadPoolExecutor createQueue(
+      int poolsize, String queueName, int threadPriority, boolean withMetrics) {
+    Executor executor = new Executor(poolsize, queueName);
+    if (withMetrics) {
+      logger.atInfo().log("Adding metrics for '%s' queue", queueName);
+      executor.buildMetrics(queueName);
+    }
     executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
     executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(true);
     queues.add(executor);
@@ -207,7 +248,7 @@
     private final ConcurrentHashMap<Integer, Task<?>> all;
     private final String queueName;
 
-    Executor(int corePoolSize, String prefix) {
+    Executor(int corePoolSize, final String queueName) {
       super(
           corePoolSize,
           new ThreadFactory() {
@@ -217,7 +258,7 @@
             @Override
             public Thread newThread(Runnable task) {
               final Thread t = parent.newThread(task);
-              t.setName(prefix + "-" + tid.getAndIncrement());
+              t.setName(queueName + "-" + tid.getAndIncrement());
               t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
               return t;
             }
@@ -229,12 +270,17 @@
               0.75f, // load factor
               corePoolSize + 4 // concurrency level
               );
-      queueName = prefix;
-      buildMetrics(queueName, metrics);
+      this.queueName = queueName;
     }
 
-    private void buildMetrics(String queueName, MetricMaker metric) {
-      metric.newCallbackMetric(
+    @Override
+    protected void terminated() {
+      super.terminated();
+      queues.remove(this);
+    }
+
+    private void buildMetrics(String queueName) {
+      metrics.newCallbackMetric(
           getMetricName(queueName, "max_pool_size"),
           Long.class,
           new Description("Maximum allowed number of threads in the pool")
@@ -246,7 +292,7 @@
               return (long) getMaximumPoolSize();
             }
           });
-      metric.newCallbackMetric(
+      metrics.newCallbackMetric(
           getMetricName(queueName, "pool_size"),
           Long.class,
           new Description("Current number of threads in the pool").setGauge().setUnit("threads"),
@@ -256,7 +302,7 @@
               return (long) getPoolSize();
             }
           });
-      metric.newCallbackMetric(
+      metrics.newCallbackMetric(
           getMetricName(queueName, "active_threads"),
           Long.class,
           new Description("Number number of threads that are actively executing tasks")
@@ -268,7 +314,7 @@
               return (long) getActiveCount();
             }
           });
-      metric.newCallbackMetric(
+      metrics.newCallbackMetric(
           getMetricName(queueName, "scheduled_tasks"),
           Integer.class,
           new Description("Number of scheduled tasks in the queue").setGauge().setUnit("tasks"),
@@ -278,7 +324,7 @@
               return getQueue().size();
             }
           });
-      metric.newCallbackMetric(
+      metrics.newCallbackMetric(
           getMetricName(queueName, "total_scheduled_tasks_count"),
           Long.class,
           new Description("Total number of tasks that have been scheduled for execution")
@@ -290,7 +336,7 @@
               return (long) getTaskCount();
             }
           });
-      metric.newCallbackMetric(
+      metrics.newCallbackMetric(
           getMetricName(queueName, "total_completed_tasks_count"),
           Long.class,
           new Description("Total number of tasks that have completed execution")
@@ -309,13 +355,7 @@
           CaseFormat.UPPER_CAMEL.to(
               CaseFormat.LOWER_UNDERSCORE,
               queueName.replaceFirst("SSH", "Ssh").replaceAll("-", ""));
-      return String.format("queue/%s/%s", name, metricName);
-    }
-
-    @Override
-    protected void terminated() {
-      super.terminated();
-      queues.remove(this);
+      return metrics.sanitizeMetricName(String.format("queue/%s/%s", name, metricName));
     }
 
     @Override
@@ -564,7 +604,8 @@
           }
         }
       } catch (ClassNotFoundException | IllegalArgumentException | IllegalAccessException e) {
-        log.debug("Cannot get a proper name for TrustedListenableFutureTask: {}", e.getMessage());
+        logger.atFine().log(
+            "Cannot get a proper name for TrustedListenableFutureTask: %s", e.getMessage());
       }
       return runnable.toString();
     }
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index b7297fa..01ce468 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git.receive;
 
 import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -63,12 +64,10 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Hook that delegates to {@link ReceiveCommits} in a worker thread. */
 public class AsyncReceiveCommits implements PreReceiveHook {
-  private static final Logger log = LoggerFactory.getLogger(AsyncReceiveCommits.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String TIMEOUT_NAME = "ReceiveCommitsOverallTimeout";
 
@@ -272,11 +271,9 @@
       w.progress.waitFor(
           executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (ExecutionException e) {
-      log.warn(
-          String.format(
-              "Error in ReceiveCommits while processing changes for project %s",
-              projectState.getName()),
-          e);
+      logger.atWarning().withCause(e).log(
+          "Error in ReceiveCommits while processing changes for project %s",
+          projectState.getName());
       rp.sendError("internal error while processing changes");
       // ReceiveCommits has tried its best to catch errors, so anything at this
       // point is very bad.
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 4d24b4b..fddb9d6 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -14,9 +14,9 @@
         "//lib:gwtorm",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
index 7f0c626..1001d04 100644
--- a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
+++ b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
@@ -17,6 +17,7 @@
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
@@ -31,8 +32,6 @@
 import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.eclipse.jgit.transport.UploadPack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Advertises part of history to git push clients.
@@ -47,7 +46,7 @@
  * a common ancestor.
  */
 public class HackPushNegotiateHook implements AdvertiseRefsHook {
-  private static final Logger log = LoggerFactory.getLogger(HackPushNegotiateHook.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Size of an additional ".have" line. */
   private static final int HAVE_LINE_LEN = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
@@ -127,7 +126,7 @@
           }
         }
       } catch (IOException err) {
-        log.error("error trying to advertise history", err);
+        logger.atSevere().withCause(err).log("error trying to advertise history");
       }
       return history;
     } finally {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index c93c1d7..a91fac5 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -56,6 +56,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
@@ -209,12 +210,10 @@
 import org.eclipse.jgit.transport.ReceivePack;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Receives change upload using the Git receive-pack protocol. */
 class ReceiveCommits {
-  private static final Logger log = LoggerFactory.getLogger(ReceiveCommits.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private enum ReceiveError {
     CONFIG_UPDATE(
@@ -581,7 +580,7 @@
     replaceProgress.end();
 
     if (!errors.isEmpty()) {
-      logDebug("Handling error conditions: {}", errors.keySet());
+      logDebug("Handling error conditions: %s", errors.keySet());
       for (ReceiveError error : errors.keySet()) {
         rp.sendMessage(buildError(error, errors.get(error)));
       }
@@ -733,20 +732,20 @@
       bu.setRequestId(receiveId);
       bu.setRefLogMessage("push");
 
-      logDebug("Adding {} replace requests", newChanges.size());
+      logDebug("Adding %d replace requests", newChanges.size());
       for (ReplaceRequest replace : replaceByChange.values()) {
         replace.addOps(bu, replaceProgress);
       }
 
-      logDebug("Adding {} create requests", newChanges.size());
+      logDebug("Adding %d create requests", newChanges.size());
       for (CreateRequest create : newChanges) {
         create.addOps(bu);
       }
 
-      logDebug("Adding {} group update requests", newChanges.size());
+      logDebug("Adding %d group update requests", newChanges.size());
       updateGroups.forEach(r -> r.addOps(bu));
 
-      logDebug("Adding {} additional ref updates", actualCommands.size());
+      logDebug("Adding %d additional ref updates", actualCommands.size());
       actualCommands.forEach(c -> bu.addRepoOnlyOp(new UpdateOneRefOp(c)));
 
       logDebug("Executing batch");
@@ -842,11 +841,11 @@
 
   private void parseCommands(Collection<ReceiveCommand> commands)
       throws PermissionBackendException, NoSuchProjectException, IOException {
-    logDebug("Parsing {} commands", commands.size());
+    logDebug("Parsing %d commands", commands.size());
     for (ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
         // Already rejected by the core receive process.
-        logDebug("Already processed by core: {} {}", cmd.getResult(), cmd);
+        logDebug("Already processed by core: %s %s", cmd.getResult(), cmd);
         continue;
       }
 
@@ -867,7 +866,7 @@
 
       if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) {
         String newName = RefNames.refsUsers(user.getAccountId());
-        logDebug("Swapping out command for {} to {}", RefNames.REFS_USERS_SELF, newName);
+        logDebug("Swapping out command for %s to %s", RefNames.REFS_USERS_SELF, newName);
         final ReceiveCommand orgCmd = cmd;
         cmd =
             new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), newName, cmd.getType()) {
@@ -898,11 +897,8 @@
         // migrate to NoteDb eventually, and we don't want garbage data waiting there when the
         // migration finishes.
         logDebug(
-            "{} NoteDb ref {} with {}={}",
-            cmd.getType(),
-            cmd.getRefName(),
-            NoteDbPushOption.OPTION_NAME,
-            noteDbPushOption);
+            "%s NoteDb ref %s with %s=%s",
+            cmd.getType(), cmd.getRefName(), NoteDbPushOption.OPTION_NAME, noteDbPushOption);
         if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
           // Only reject this command, not the whole push. This supports the use case of "git clone
           // --mirror" followed by "git push --mirror", when the user doesn't really intend to clone
@@ -951,7 +947,7 @@
       }
 
       if (isConfig(cmd)) {
-        logDebug("Processing {} command", cmd.getRefName());
+        logDebug("Processing %s command", cmd.getRefName());
         try {
           permissions.check(ProjectPermission.WRITE_CONFIG);
         } catch (AuthException e) {
@@ -1090,7 +1086,7 @@
       reject(cmd, "invalid object");
       return;
     }
-    logDebug("Creating {}", cmd);
+    logDebug("Creating %s", cmd);
 
     if (isHead(cmd) && !isCommit(cmd)) {
       return;
@@ -1116,7 +1112,7 @@
   }
 
   private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
-    logDebug("Updating {}", cmd);
+    logDebug("Updating %s", cmd);
     boolean ok;
     try {
       permissions.ref(cmd.getRefName()).check(RefPermission.UPDATE);
@@ -1161,7 +1157,7 @@
   }
 
   private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException {
-    logDebug("Deleting {}", cmd);
+    logDebug("Deleting %s", cmd);
     if (cmd.getRefName().startsWith(REFS_CHANGES)) {
       errors.put(ReceiveError.DELETE_CHANGES, cmd.getRefName());
       reject(cmd, "cannot delete changes");
@@ -1205,7 +1201,7 @@
       reject(cmd, "invalid object");
       return;
     }
-    logDebug("Rewinding {}", cmd);
+    logDebug("Rewinding %s", cmd);
 
     if (newObject != null) {
       validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
@@ -1255,11 +1251,10 @@
     String topic;
 
     @Option(
-      name = "--draft",
-      usage =
-          "Will be removed. Before that, this option will be mapped to '--private'"
-              + "for new changes and '--edit' for existing changes"
-    )
+        name = "--draft",
+        usage =
+            "Will be removed. Before that, this option will be mapped to '--private'"
+                + "for new changes and '--edit' for existing changes")
     boolean draft;
 
     boolean publish;
@@ -1271,20 +1266,18 @@
     boolean removePrivate;
 
     @Option(
-      name = "--wip",
-      aliases = {"-work-in-progress"},
-      usage = "mark change as work in progress"
-    )
+        name = "--wip",
+        aliases = {"-work-in-progress"},
+        usage = "mark change as work in progress")
     boolean workInProgress;
 
     @Option(name = "--ready", usage = "mark change as ready")
     boolean ready;
 
     @Option(
-      name = "--edit",
-      aliases = {"-e"},
-      usage = "upload as change edit"
-    )
+        name = "--edit",
+        aliases = {"-e"},
+        usage = "upload as change edit")
     boolean edit;
 
     @Option(name = "--submit", usage = "immediately submit the change")
@@ -1297,19 +1290,17 @@
     private boolean publishComments;
 
     @Option(
-      name = "--no-publish-comments",
-      aliases = {"--np"},
-      usage = "do not publish draft comments"
-    )
+        name = "--no-publish-comments",
+        aliases = {"--np"},
+        usage = "do not publish draft comments")
     private boolean noPublishComments;
 
     @Option(
-      name = "--notify",
-      usage =
-          "Notify handling that defines to whom email notifications "
-              + "should be sent. Allowed values are NONE, OWNER, "
-              + "OWNER_REVIEWERS, ALL. If not set, the default is ALL."
-    )
+        name = "--notify",
+        usage =
+            "Notify handling that defines to whom email notifications "
+                + "should be sent. Allowed values are NONE, OWNER, "
+                + "OWNER_REVIEWERS, ALL. If not set, the default is ALL.")
     private NotifyHandling notify;
 
     @Option(name = "--notify-to", metaVar = "USER", usage = "user that should be notified")
@@ -1322,11 +1313,10 @@
     List<Account.Id> bccs = new ArrayList<>();
 
     @Option(
-      name = "--reviewer",
-      aliases = {"-r"},
-      metaVar = "EMAIL",
-      usage = "add reviewer to changes"
-    )
+        name = "--reviewer",
+        aliases = {"-r"},
+        metaVar = "EMAIL",
+        usage = "add reviewer to changes")
     void reviewer(Account.Id id) {
       reviewer.add(id);
     }
@@ -1337,11 +1327,10 @@
     }
 
     @Option(
-      name = "--label",
-      aliases = {"-l"},
-      metaVar = "LABEL+VALUE",
-      usage = "label(s) to assign (defaults to +1 if no value provided"
-    )
+        name = "--label",
+        aliases = {"-l"},
+        metaVar = "LABEL+VALUE",
+        usage = "label(s) to assign (defaults to +1 if no value provided")
     void addLabel(String token) throws CmdLineException {
       LabelVote v = LabelVote.parse(token);
       try {
@@ -1354,11 +1343,10 @@
     }
 
     @Option(
-      name = "--message",
-      aliases = {"-m"},
-      metaVar = "MESSAGE",
-      usage = "Comment message to apply to the review"
-    )
+        name = "--message",
+        aliases = {"-m"},
+        metaVar = "MESSAGE",
+        usage = "Comment message to apply to the review")
     void addMessage(String token) {
       // Many characters have special meaning in the context of a git ref.
       //
@@ -1376,11 +1364,10 @@
     }
 
     @Option(
-      name = "--hashtag",
-      aliases = {"-t"},
-      metaVar = "HASHTAG",
-      usage = "add hashtag to changes"
-    )
+        name = "--hashtag",
+        aliases = {"-t"},
+        metaVar = "HASHTAG",
+        usage = "add hashtag to changes")
     void addHashtag(String token) throws CmdLineException {
       if (!notesMigration.readChanges()) {
         throw clp.reject("cannot add hashtags; noteDb is disabled");
@@ -1519,7 +1506,7 @@
       return;
     }
 
-    logDebug("Found magic branch {}", cmd.getRefName());
+    logDebug("Found magic branch %s", cmd.getRefName());
     magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration);
     magicBranch.reviewer.addAll(extraReviewers.get(ReviewerStateInternal.REVIEWER));
     magicBranch.cc.addAll(extraReviewers.get(ReviewerStateInternal.CC));
@@ -1553,13 +1540,13 @@
       return;
     }
     if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
-      logDebug("Handling {}", RefNames.REFS_USERS_SELF);
+      logDebug("Handling %s", RefNames.REFS_USERS_SELF);
       ref = RefNames.refsUsers(user.getAccountId());
     }
     if (!rp.getAdvertisedRefs().containsKey(ref)
         && !ref.equals(readHEAD(repo))
         && !ref.equals(RefNames.REFS_CONFIG)) {
-      logDebug("Ref {} not found", ref);
+      logDebug("Ref %s not found", ref);
       if (ref.startsWith(Constants.R_HEADS)) {
         String n = ref.substring(Constants.R_HEADS.length());
         reject(cmd, "branch " + n + " not found");
@@ -1632,7 +1619,7 @@
     RevCommit tip;
     try {
       tip = walk.parseCommit(magicBranch.cmd.getNewId());
-      logDebug("Tip of push: {}", tip.name());
+      logDebug("Tip of push: %s", tip.name());
     } catch (IOException ex) {
       magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
       logError("Invalid pack upload; one or more objects weren't sent", ex);
@@ -1667,7 +1654,7 @@
       }
 
       if (magicBranch.base != null) {
-        logDebug("Handling %base: {}", magicBranch.base);
+        logDebug("Handling %base: %s", magicBranch.base);
         magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
         for (ObjectId id : magicBranch.base) {
           try {
@@ -1690,7 +1677,7 @@
           return; // readBranchTip already rejected cmd.
         }
         magicBranch.baseCommit = Collections.singletonList(branchTip);
-        logDebug("Set baseCommit = {}", magicBranch.baseCommit.get(0).name());
+        logDebug("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
       }
     } catch (IOException ex) {
       logWarn(
@@ -1714,7 +1701,7 @@
         return;
       }
       RevCommit h = walk.parseCommit(targetRef.getObjectId());
-      logDebug("Current branch tip: {}", h.name());
+      logDebug("Current branch tip: %s", h.name());
       RevFilter oldRevFilter = walk.getRevFilter();
       try {
         walk.reset();
@@ -1738,7 +1725,7 @@
     try {
       return repo.getFullBranch();
     } catch (IOException e) {
-      log.error("Cannot read HEAD symref", e);
+      logger.atSevere().withCause(e).log("Cannot read HEAD symref");
       return null;
     }
   }
@@ -1762,7 +1749,7 @@
     RevCommit newCommit;
     try {
       newCommit = rp.getRevWalk().parseCommit(cmd.getNewId());
-      logDebug("Replacing with {}", newCommit);
+      logDebug("Replacing with %s", newCommit);
     } catch (IOException e) {
       logError("Cannot parse " + cmd.getNewId().name() + " as commit", e);
       reject(cmd, "invalid commit");
@@ -1786,7 +1773,7 @@
       return;
     }
 
-    logDebug("Replacing change {}", changeEnt.getId());
+    logDebug("Replacing change %s", changeEnt.getId());
     requestReplace(cmd, true, changeEnt, newCommit);
   }
 
@@ -1895,7 +1882,7 @@
         }
         int n = pending.size() + newChanges.size();
         if (maxBatchChanges != 0 && n > maxBatchChanges) {
-          logDebug("{} changes exceeds limit of {}", n, maxBatchChanges);
+          logDebug("%d changes exceeds limit of %d", n, maxBatchChanges);
           reject(
               magicBranch.cmd,
               "the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
@@ -1915,7 +1902,7 @@
             continue;
           }
 
-          logDebug("Creating new change for {} even though it is already tracked", name);
+          logDebug("Creating new change for %s even though it is already tracked", name);
         }
 
         if (!validCommit(
@@ -1932,7 +1919,7 @@
               magicBranch.cmd,
               "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
                   + "to override please set the base manually");
-          logDebug("Rejecting merge commit {} with newChangeForAllNotInTarget", name);
+          logDebug("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
           // TODO(dborowitz): Should we early return here?
         }
 
@@ -1942,13 +1929,10 @@
         }
       }
       logDebug(
-          "Finished initial RevWalk with {} commits total: {} already"
-              + " tracked, {} new changes with no Change-Id, and {} deferred"
+          "Finished initial RevWalk with %d commits total: %d already"
+              + " tracked, %d new changes with no Change-Id, and %d deferred"
               + " lookups",
-          total,
-          alreadyTracked,
-          newChanges.size(),
-          pending.size());
+          total, alreadyTracked, newChanges.size(), pending.size());
 
       if (rejectImplicitMerges) {
         rejectImplicitMerges(mergedParents);
@@ -1961,7 +1945,7 @@
         }
 
         if (newChangeIds.contains(p.changeKey)) {
-          logDebug("Multiple commits with Change-Id {}", p.changeKey);
+          logDebug("Multiple commits with Change-Id %s", p.changeKey);
           reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
           newChanges = Collections.emptyList();
           return;
@@ -1970,7 +1954,7 @@
         List<ChangeData> changes = p.destChanges;
         if (changes.size() > 1) {
           logDebug(
-              "Multiple changes in branch {} with Change-Id {}: {}",
+              "Multiple changes in branch %s with Change-Id %s: %s",
               magicBranch.dest,
               p.changeKey,
               changes.stream().map(cd -> cd.getId().toString()).collect(joining()));
@@ -2032,9 +2016,8 @@
         newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get()));
       }
       logDebug(
-          "Finished deferred lookups with {} updates and {} new changes",
-          replaceByChange.size(),
-          newChanges.size());
+          "Finished deferred lookups with %d updates and %d new changes",
+          replaceByChange.size(), newChanges.size());
     } catch (IOException e) {
       // Should never happen, the core receive process would have
       // identified the missing object earlier before we got control.
@@ -2087,7 +2070,7 @@
           notesFactory.create(db, project.getNameKey(), Change.Id.fromRef(ref.getName()));
       Change change = notes.getChange();
       if (change.getDest().equals(magicBranch.dest)) {
-        logDebug("Found change {} from existing refs.", change.getKey());
+        logDebug("Found change %s from existing refs.", change.getKey());
         // Reindex the change asynchronously, ignoring errors.
         @SuppressWarnings("unused")
         Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId());
@@ -2108,7 +2091,7 @@
     if (magicBranch.baseCommit != null) {
       markExplicitBasesUninteresting();
     } else if (magicBranch.merged) {
-      logDebug("Marking parents of merged commit {} uninteresting", start.name());
+      logDebug("Marking parents of merged commit %s uninteresting", start.name());
       for (RevCommit c : start.getParents()) {
         rw.markUninteresting(c);
       }
@@ -2119,16 +2102,15 @@
   }
 
   private void markExplicitBasesUninteresting() throws IOException {
-    logDebug("Marking {} base commits uninteresting", magicBranch.baseCommit.size());
+    logDebug("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
     for (RevCommit c : magicBranch.baseCommit) {
       rp.getRevWalk().markUninteresting(c);
     }
     Ref targetRef = allRefs().get(magicBranch.dest.get());
     if (targetRef != null) {
       logDebug(
-          "Marking target ref {} ({}) uninteresting",
-          magicBranch.dest.get(),
-          targetRef.getObjectId().name());
+          "Marking target ref %s (%s) uninteresting",
+          magicBranch.dest.get(), targetRef.getObjectId().name());
       rp.getRevWalk().markUninteresting(rp.getRevWalk().parseCommit(targetRef.getObjectId()));
     }
   }
@@ -2180,7 +2162,7 @@
         }
       }
     }
-    logDebug("Marked {} heads as uninteresting", i);
+    logDebug("Marked %d heads as uninteresting", i);
   }
 
   private static boolean isValidChangeId(String idStr) {
@@ -2324,7 +2306,7 @@
     checkNotNull(
         tipChange, "tip of push does not correspond to a change; found these changes: %s", bySha);
     logDebug(
-        "Processing submit with tip change {} ({})", tipChange.getId(), magicBranch.cmd.getNewId());
+        "Processing submit with tip change %s (%s)", tipChange.getId(), magicBranch.cmd.getNewId());
     try (MergeOp op = mergeOpProvider.get()) {
       op.merge(db, tipChange, user, false, new SubmitInput(), false);
     }
@@ -2363,7 +2345,7 @@
         }
       }
     }
-    logDebug("Read {} changes to replace", replaceByChange.size());
+    logDebug("Read %d changes to replace", replaceByChange.size());
 
     if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
       // Cancel creations tied to refs/for/ or refs/drafts/ command.
@@ -2699,7 +2681,7 @@
     public void postUpdate(Context ctx) {
       String refName = cmd.getRefName();
       if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
-        logDebug("Updating tag cache on fast-forward of {}", cmd.getRefName());
+        logDebug("Updating tag cache on fast-forward of %s", cmd.getRefName());
         tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId());
       }
       if (isConfig(cmd)) {
@@ -2707,20 +2689,21 @@
         try {
           projectCache.evict(project);
         } catch (IOException e) {
-          log.warn("Cannot evict from project cache, name key: " + project.getName(), e);
+          logger.atWarning().withCause(e).log(
+              "Cannot evict from project cache, name key: %s", project.getName());
         }
         ProjectState ps = projectCache.get(project.getNameKey());
         try {
           logDebug("Updating project description");
           repo.setGitwebDescription(ps.getProject().getDescription());
         } catch (IOException e) {
-          log.warn("cannot update description of " + project.getName(), e);
+          logger.atWarning().withCause(e).log("cannot update description of %s", project.getName());
         }
         if (allProjectsName.equals(project.getNameKey())) {
           try {
             createGroupPermissionSyncer.syncIfNeeded();
           } catch (IOException | ConfigInvalidException e) {
-            log.error("Can't sync create group permissions", e);
+            logger.atSevere().withCause(e).log("Can't sync create group permissions");
           }
         }
       }
@@ -2850,7 +2833,7 @@
       int n = 0;
       for (RevCommit c; (c = walk.next()) != null; ) {
         if (++n > limit) {
-          logDebug("Number of new commits exceeds limit of {}", limit);
+          logDebug("Number of new commits exceeds limit of %d", limit);
           addMessage(
               String.format(
                   "Cannot push more than %d commits to %s without %s option "
@@ -2871,7 +2854,7 @@
           missingFullName = false;
         }
       }
-      logDebug("Validated {} new commits", n);
+      logDebug("Validated %d new commits", n);
     } catch (IOException err) {
       cmd.setResult(REJECTED_MISSING_OBJECT);
       logError("Invalid pack upload; one or more objects weren't sent", err);
@@ -2908,7 +2891,7 @@
                   perm, branch, user.asIdentifiedUser(), sshInfo, repo, rw, change);
       messages.addAll(validators.validate(receiveEvent));
     } catch (CommitValidationException e) {
-      logDebug("Commit validation failed on {}", c.name());
+      logDebug("Commit validation failed on %s", c.name());
       messages.addAll(e.getMessages());
       reject(cmd, e.getMessage());
       return false;
@@ -2959,9 +2942,8 @@
 
                 for (Ref ref : byCommit.get(c.copy())) {
                   PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
-                  Optional<ChangeData> cd =
-                      executeIndexQuery(() -> byLegacyId(psId.getParentKey()));
-                  if (cd.isPresent() && cd.get().change().getDest().equals(branch)) {
+                  Optional<ChangeNotes> notes = getChangeNotes(psId.getParentKey());
+                  if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
                     existingPatchSets++;
                     bu.addOp(
                         psId.getParentKey(),
@@ -2991,7 +2973,7 @@
               for (ReplaceRequest req : replaceAndClose) {
                 Change.Id id = req.notes.getChangeId();
                 if (!executeRequestValidation(() -> req.validate(true))) {
-                  logDebug("Not closing {} because validation failed", id);
+                  logDebug("Not closing %s because validation failed", id);
                   continue;
                 }
                 req.addOps(bu, null);
@@ -3004,9 +2986,8 @@
               }
 
               logDebug(
-                  "Auto-closing {} changes with existing patch sets and {} with new patch sets",
-                  existingPatchSets,
-                  newPatchSets);
+                  "Auto-closing %s changes with existing patch sets and %s with new patch sets",
+                  existingPatchSets, newPatchSets);
               bu.execute();
             } catch (IOException | OrmException | PermissionBackendException e) {
               logError("Failed to auto-close changes", e);
@@ -3025,6 +3006,14 @@
     }
   }
 
+  private Optional<ChangeNotes> getChangeNotes(Change.Id changeId) throws OrmException {
+    try {
+      return Optional.of(notesFactory.createChecked(db, project.getNameKey(), changeId));
+    } catch (NoSuchChangeException e) {
+      return Optional.empty();
+    }
+  }
+
   private <T> T executeIndexQuery(Action<T> action) throws OrmException {
     try {
       return retryHelper.execute(ActionType.INDEX_QUERY, action, OrmException.class::isInstance);
@@ -3087,14 +3076,6 @@
     return r;
   }
 
-  private Optional<ChangeData> byLegacyId(Change.Id legacyId) throws OrmException {
-    List<ChangeData> res = queryProvider.get().byLegacyChangeId(legacyId);
-    if (res.isEmpty()) {
-      return Optional.empty();
-    }
-    return Optional.of(res.get(0));
-  }
-
   private Map<String, Ref> allRefs() {
     return allRefsWatcher.getAllRefs();
   }
@@ -3114,20 +3095,34 @@
     return cmd.getRefName().equals(RefNames.REFS_CONFIG);
   }
 
-  private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(receiveId + msg, args);
-    }
+  private void logDebug(String msg) {
+    logger.atFine().log(receiveId + msg);
+  }
+
+  private void logDebug(String msg, @Nullable Object arg) {
+    logger.atFine().log(receiveId + msg, arg);
+  }
+
+  private void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+    logger.atFine().log(receiveId + msg, arg1, arg2);
+  }
+
+  private void logDebug(
+      String msg, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
+    logger.atFine().log(receiveId + msg, arg1, arg2, arg3);
+  }
+
+  private void logDebug(
+      String msg,
+      @Nullable Object arg1,
+      @Nullable Object arg2,
+      @Nullable Object arg3,
+      @Nullable Object arg4) {
+    logger.atFine().log(receiveId + msg, arg1, arg2, arg3, arg4);
   }
 
   private void logWarn(String msg, Throwable t) {
-    if (log.isWarnEnabled()) {
-      if (t != null) {
-        log.warn(receiveId + msg, t);
-      } else {
-        log.warn(receiveId + msg);
-      }
-    }
+    logger.atWarning().withCause(t).log("%s%s", receiveId, msg);
   }
 
   private void logWarn(String msg) {
@@ -3135,13 +3130,7 @@
   }
 
   private void logError(String msg, Throwable t) {
-    if (log.isErrorEnabled()) {
-      if (t != null) {
-        log.error(receiveId + msg, t);
-      } else {
-        log.error(receiveId + msg);
-      }
-    }
+    logger.atSevere().withCause(t).log("%s%s", receiveId, msg);
   }
 
   private void logError(String msg) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 723fef4..8cbcc88 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -18,6 +18,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -37,12 +38,10 @@
 import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
 import org.eclipse.jgit.transport.UploadPack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Exposes only the non refs/changes/ reference names. */
 public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
-  private static final Logger log = LoggerFactory.getLogger(ReceiveCommitsAdvertiseRefsHook.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @VisibleForTesting
   @AutoValue
@@ -120,7 +119,7 @@
       }
       return r;
     } catch (OrmException err) {
-      log.error("Cannot list open changes of " + projectName, err);
+      logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName);
       return Collections.emptySet();
     }
   }
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 3b8091c..36c5005 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.PublishCommentUtil;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.EmailReviewComments;
@@ -78,10 +80,10 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ReplaceOp implements BatchUpdateOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public interface Factory {
     ReplaceOp create(
         ProjectState projectState,
@@ -97,8 +99,6 @@
         @Nullable PushCertificate pushCertificate);
   }
 
-  private static final Logger log = LoggerFactory.getLogger(ReplaceOp.class);
-
   private static final String CHANGE_IS_CLOSED = "change is closed";
 
   private final AccountResolver accountResolver;
@@ -108,6 +108,7 @@
   private final ChangeKindCache changeKindCache;
   private final ChangeMessagesUtil cmUtil;
   private final CommentsUtil commentsUtil;
+  private final PublishCommentUtil publishCommentUtil;
   private final EmailReviewComments.Factory emailCommentsFactory;
   private final ExecutorService sendEmailExecutor;
   private final RevisionCreated revisionCreated;
@@ -151,6 +152,7 @@
       ChangeKindCache changeKindCache,
       ChangeMessagesUtil cmUtil,
       CommentsUtil commentsUtil,
+      PublishCommentUtil publishCommentUtil,
       EmailReviewComments.Factory emailCommentsFactory,
       RevisionCreated revisionCreated,
       CommentAdded commentAdded,
@@ -177,6 +179,7 @@
     this.changeKindCache = changeKindCache;
     this.cmUtil = cmUtil;
     this.commentsUtil = commentsUtil;
+    this.publishCommentUtil = publishCommentUtil;
     this.emailCommentsFactory = emailCommentsFactory;
     this.revisionCreated = revisionCreated;
     this.commentAdded = commentAdded;
@@ -447,7 +450,7 @@
       throws OrmException {
     List<Comment> comments =
         commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), ctx.getUser().getAccountId());
-    commentsUtil.publish(
+    publishCommentUtil.publish(
         ctx, patchSetId, comments, ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
     return comments;
   }
@@ -486,7 +489,7 @@
     try {
       fireCommentAddedEvent(ctx);
     } catch (Exception e) {
-      log.warn("comment-added event invocation failed", e);
+      logger.atWarning().withCause(e).log("comment-added event invocation failed");
     }
     if (mergedByPushOp != null) {
       mergedByPushOp.postUpdate(ctx);
@@ -516,7 +519,8 @@
         cm.addExtraCC(recipients.getCcOnly());
         cm.send();
       } catch (Exception e) {
-        log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+        logger.atSevere().withCause(e).log(
+            "Cannot send email for new patch set %s", newPatchSet.getId());
       }
     }
 
@@ -600,7 +604,7 @@
       }
       return null;
     } catch (IOException e) {
-      log.warn("Can't check for already submitted change", e);
+      logger.atWarning().withCause(e).log("Can't check for already submitted change");
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index e2ab1e9..932d1f8 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
@@ -77,11 +78,9 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.SystemReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class CommitValidators {
-  private static final Logger log = LoggerFactory.getLogger(CommitValidators.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final Pattern NEW_PATCHSET_PATTERN =
       Pattern.compile("^" + REFS_CHANGES + "(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/[1-9][0-9]*)?$");
@@ -227,7 +226,8 @@
         messages.addAll(commitValidator.onCommitReceived(receiveEvent));
       }
     } catch (CommitValidationException e) {
-      log.debug("CommitValidationException occurred: {}", e.getFullMessage(), e);
+      logger.atFine().withCause(e).log(
+          "CommitValidationException occurred: %s", e.getFullMessage());
       // Keep the old messages (and their order) in case of an exception
       messages.addAll(e.getMessages());
       throw new CommitValidationException(e.getMessage(), messages);
@@ -443,14 +443,11 @@
             throw new ConfigInvalidException("invalid project configuration");
           }
         } catch (ConfigInvalidException | IOException e) {
-          log.error(
-              "User "
-                  + user.getLoggableName()
-                  + " tried to push an invalid project configuration "
-                  + receiveEvent.command.getNewId().name()
-                  + " for project "
-                  + receiveEvent.project.getName(),
-              e);
+          logger.atSevere().withCause(e).log(
+              "User %s tried to push an invalid project configuration %s for project %s",
+              user.getLoggableName(),
+              receiveEvent.command.getNewId().name(),
+              receiveEvent.project.getName());
           throw new CommitValidationException("invalid project configuration", messages);
         }
       }
@@ -479,7 +476,7 @@
       } catch (AuthException e) {
         throw new CommitValidationException("you are not allowed to upload merges");
       } catch (PermissionBackendException e) {
-        log.error("cannot check MERGE", e);
+        logger.atSevere().withCause(e).log("cannot check MERGE");
         throw new CommitValidationException("internal auth error");
       }
     }
@@ -554,7 +551,7 @@
           throw new CommitValidationException(
               "not Signed-off-by author/committer/uploader in commit message footer");
         } catch (PermissionBackendException e) {
-          log.error("cannot check FORGE_COMMITTER", e);
+          logger.atSevere().withCause(e).log("cannot check FORGE_COMMITTER");
           throw new CommitValidationException("internal auth error");
         }
       }
@@ -590,7 +587,7 @@
             "invalid author",
             invalidEmail(receiveEvent.commit, "author", author, user, canonicalWebUrl));
       } catch (PermissionBackendException e) {
-        log.error("cannot check FORGE_AUTHOR", e);
+        logger.atSevere().withCause(e).log("cannot check FORGE_AUTHOR");
         throw new CommitValidationException("internal auth error");
       }
     }
@@ -624,7 +621,7 @@
             "invalid committer",
             invalidEmail(receiveEvent.commit, "committer", committer, user, canonicalWebUrl));
       } catch (PermissionBackendException e) {
-        log.error("cannot check FORGE_COMMITTER", e);
+        logger.atSevere().withCause(e).log("cannot check FORGE_COMMITTER");
         throw new CommitValidationException("internal auth error");
       }
     }
@@ -663,7 +660,7 @@
                   gerritIdent.getEmailAddress(),
                   RefPermission.FORGE_SERVER.name()));
         } catch (PermissionBackendException e) {
-          log.error("cannot check FORGE_SERVER", e);
+          logger.atSevere().withCause(e).log("cannot check FORGE_SERVER");
           throw new CommitValidationException("internal auth error");
         }
       }
@@ -690,7 +687,7 @@
         return Collections.emptyList();
       } catch (IOException e) {
         String m = "error checking banned commits";
-        log.warn(m, e);
+        logger.atWarning().withCause(e).log(m);
         throw new CommitValidationException(m, e);
       }
     }
@@ -729,7 +726,7 @@
           return msgs;
         } catch (IOException | ConfigInvalidException e) {
           String m = "error validating external IDs";
-          log.warn(m, e);
+          logger.atWarning().withCause(e).log(m);
           throw new CommitValidationException(m, e);
         }
       }
@@ -787,7 +784,7 @@
         }
       } catch (IOException e) {
         String m = String.format("Validating update for account %s failed", accountId.get());
-        log.error(m, e);
+        logger.atSevere().withCause(e).log(m);
         throw new CommitValidationException(m, e);
       }
       return Collections.emptyList();
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 5579e0e..94d9996 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
@@ -49,11 +50,9 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class MergeValidators {
-  private static final Logger log = LoggerFactory.getLogger(MergeValidators.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<MergeValidationListener> mergeValidationListeners;
   private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
@@ -168,7 +167,7 @@
               } catch (AuthException e) {
                 throw new MergeValidationException(SET_BY_ADMIN);
               } catch (PermissionBackendException e) {
-                log.warn("Cannot check ADMINISTRATE_SERVER", e);
+                logger.atWarning().withCause(e).log("Cannot check ADMINISTRATE_SERVER");
                 throw new MergeValidationException("validation unavailable");
               }
               if (allUsersName.equals(destProject.getNameKey())
@@ -280,7 +279,7 @@
           return;
         }
       } catch (IOException | OrmException e) {
-        log.error("Cannot validate account update", e);
+        logger.atSevere().withCause(e).log("Cannot validate account update");
         throw new MergeValidationException("account validation unavailable");
       }
 
@@ -291,7 +290,7 @@
               "invalid account configuration: " + Joiner.on("; ").join(errorMessages));
         }
       } catch (IOException e) {
-        log.error("Cannot validate account update", e);
+        logger.atSevere().withCause(e).log("Cannot validate account update");
         throw new MergeValidationException("account validation unavailable");
       }
     }
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 1e031da..1df8da4 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -16,6 +16,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -34,12 +35,11 @@
 import java.util.List;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class RefOperationValidators {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final GetErrorMessages GET_ERRORS = new GetErrorMessages();
-  private static final Logger LOG = LoggerFactory.getLogger(RefOperationValidators.class);
 
   public interface Factory {
     RefOperationValidators create(Project project, IdentifiedUser user, ReceiveCommand cmd);
@@ -101,7 +101,7 @@
         String.format(
             "Ref \"%s\" %S in project %s validation failed",
             event.command.getRefName(), event.command.getType(), event.project.getName());
-    LOG.error(header);
+    logger.atSevere().log(header);
     throw new RefOperationValidationException(header, errors);
   }
 
diff --git a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
index d30945f..dbbc3f6 100644
--- a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -35,8 +36,6 @@
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Runnable to schedule periodic group reindexing.
@@ -58,7 +57,7 @@
  * slave.
  */
 public class PeriodicGroupIndexer implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(PeriodicGroupIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends LifecycleModule {
     @Override
@@ -88,7 +87,7 @@
 
       boolean isEnabled = cfg.getBoolean("index", "scheduledIndexer", "enabled", true);
       if (!isEnabled) {
-        log.warn("index.scheduledIndexer is disabled");
+        logger.atWarning().log("index.scheduledIndexer is disabled");
         return;
       }
 
@@ -146,9 +145,9 @@
         }
       }
       groupUuids = newGroupUuids;
-      log.info("Run group indexer, {} groups reindexed", reindexCounter);
+      logger.atInfo().log("Run group indexer, %s groups reindexed", reindexCounter);
     } catch (Throwable t) {
-      log.error("Failed to reindex groups", t);
+      logger.atSevere().withCause(t).log("Failed to reindex groups");
     }
   }
 }
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index 967b0d2..c6d1a6f 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
@@ -41,13 +42,11 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** NoteDb reader for group audit log. */
 @Singleton
 public class AuditLogReader {
-  private static final Logger log = LoggerFactory.getLogger(AuditLogReader.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final String serverId;
 
@@ -184,11 +183,9 @@
   }
 
   private static void logInvalid(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
-    log.debug(
-        "Invalid footer line in commit {} while parsing audit log for group {}: {}",
-        c.name(),
-        uuid,
-        line);
+    logger.atFine().log(
+        "Invalid footer line in commit %s while parsing audit log for group %s: %s",
+        c.name(), uuid, line);
   }
 
   private ImmutableList<ParsedCommit> parseCommits(Repository repo, AccountGroup.UUID uuid)
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 9f0cb3a..b5324f1 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
@@ -46,13 +47,12 @@
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Check the referential integrity of NoteDb group storage. */
 @Singleton
 public class GroupsNoteDbConsistencyChecker {
-  private static final Logger log = LoggerFactory.getLogger(GroupsNoteDbConsistencyChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   /**
    * The result of a consistency check. The UUID map is only non-null if no problems were detected.
    */
@@ -271,9 +271,9 @@
 
   public static void logConsistencyProblem(ConsistencyProblemInfo p) {
     if (p.status == ConsistencyProblemInfo.Status.WARNING) {
-      log.warn(p.message);
+      logger.atWarning().log(p.message);
     } else {
-      log.error(p.message);
+      logger.atSevere().log(p.message);
     }
   }
 
diff --git a/java/com/google/gerrit/server/group/db/RenameGroupOp.java b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
index 57acc3b..eada57d 100644
--- a/java/com/google/gerrit/server/group/db/RenameGroupOp.java
+++ b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group.db;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -32,10 +33,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class RenameGroupOp extends DefaultQueueOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   interface Factory {
     RenameGroupOp create(
         @Assisted("author") PersonIdent author,
@@ -45,7 +46,6 @@
   }
 
   private static final int MAX_TRIES = 10;
-  private static final Logger log = LoggerFactory.getLogger(RenameGroupOp.class);
 
   private final ProjectCache projectCache;
   private final MetaDataUpdate.Server metaDataUpdateFactory;
@@ -93,7 +93,7 @@
       } catch (RepositoryNotFoundException noProject) {
         continue;
       } catch (ConfigInvalidException | IOException err) {
-        log.error("Cannot rename group " + oldName + " in " + projectName, err);
+        logger.atSevere().withCause(err).log("Cannot rename group %s in %s", oldName, projectName);
       }
     }
 
@@ -127,14 +127,9 @@
         projectCache.evict(config.getProject());
         success = true;
       } catch (IOException e) {
-        log.error(
-            "Could not commit rename of group "
-                + oldName
-                + " to "
-                + newName
-                + " in "
-                + md.getProjectName().get(),
-            e);
+        logger.atSevere().withCause(e).log(
+            "Could not commit rename of group %s to %s in %s",
+            oldName, newName, md.getProjectName().get());
         try {
           Thread.sleep(25 /* milliseconds */);
         } catch (InterruptedException wakeUp) {
@@ -145,13 +140,8 @@
 
     if (!success) {
       if (tryingAgain) {
-        log.warn(
-            "Could not rename group "
-                + oldName
-                + " to "
-                + newName
-                + " in "
-                + md.getProjectName().get());
+        logger.atWarning().log(
+            "Could not rename group %s to %s in %s", oldName, newName, md.getProjectName().get());
       } else {
         retryOn.add(md.getProjectName());
       }
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index c069864..d957558 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -217,7 +217,8 @@
     if (threads <= 0) {
       threads = Runtime.getRuntime().availableProcessors() / 2 + 1;
     }
-    return MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "Index-Interactive"));
+    return MoreExecutors.listeningDecorator(
+        workQueue.createQueue(threads, "Index-Interactive", true));
   }
 
   @Provides
@@ -232,7 +233,7 @@
     if (threads <= 0) {
       threads = Runtime.getRuntime().availableProcessors();
     }
-    return MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "Index-Batch"));
+    return MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "Index-Batch", true));
   }
 
   @Singleton
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java
index ee2a76e..0695278 100644
--- a/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexCollection;
@@ -25,11 +26,9 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class OnlineReindexer<K, V, I extends Index<K, V>> {
-  private static final Logger log = LoggerFactory.getLogger(OnlineReindexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final String name;
   private final IndexCollection<K, V, I> indexes;
@@ -64,7 +63,8 @@
                 reindex();
                 ok = true;
               } catch (IOException e) {
-                log.error("Online reindex of {} schema version {} failed", name, version(index), e);
+                logger.atSevere().withCause(e).log(
+                    "Online reindex of %s schema version %s failed", name, version(index));
               } finally {
                 running.set(false);
                 if (!ok) {
@@ -103,27 +103,22 @@
             "not an active write schema version: %s %s",
             name,
             newVersion);
-    log.info(
-        "Starting online reindex of {} from schema version {} to {}",
-        name,
-        version(indexes.getSearchIndex()),
-        version(index));
+    logger.atInfo().log(
+        "Starting online reindex of %s from schema version %s to %s",
+        name, version(indexes.getSearchIndex()), version(index));
 
     if (oldVersion != newVersion) {
       index.deleteAll();
     }
     SiteIndexer.Result result = batchIndexer.indexAll(index);
     if (!result.success()) {
-      log.error(
-          "Online reindex of {} schema version {} failed. Successfully"
-              + " indexed {}, failed to index {}",
-          name,
-          version(index),
-          result.doneCount(),
-          result.failedCount());
+      logger.atSevere().log(
+          "Online reindex of %s schema version %s failed. Successfully"
+              + " indexed %s, failed to index %s",
+          name, version(index), result.doneCount(), result.failedCount());
       return;
     }
-    log.info("Reindex {} to version {} complete", name, version(index));
+    logger.atInfo().log("Reindex %s to version %s complete", name, version(index));
     activateIndex();
     for (OnlineUpgradeListener listener : listeners) {
       listener.onSuccess(name, oldVersion, newVersion);
@@ -132,11 +127,11 @@
 
   public void activateIndex() {
     indexes.setSearchIndex(index);
-    log.info("Using {} schema version {}", name, version(index));
+    logger.atInfo().log("Using %s schema version %s", name, version(index));
     try {
       index.markReady(true);
     } catch (IOException e) {
-      log.warn("Error activating new {} schema version {}", name, version(index));
+      logger.atWarning().log("Error activating new %s schema version %s", name, version(index));
     }
 
     List<I> toRemove = Lists.newArrayListWithExpectedSize(1);
@@ -150,7 +145,7 @@
         i.markReady(false);
         indexes.removeWriteIndex(version(i));
       } catch (IOException e) {
-        log.warn("Error deactivating old {} schema version {}", name, version(i));
+        logger.atWarning().log("Error deactivating old %s schema version %s", name, version(i));
       }
     }
   }
diff --git a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
index ae1fec6..0015268 100644
--- a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -38,12 +39,10 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AllAccountsIndexer extends SiteIndexer<Account.Id, AccountState, AccountIndex> {
-  private static final Logger log = LoggerFactory.getLogger(AllAccountsIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ListeningExecutorService executor;
   private final Accounts accounts;
@@ -68,7 +67,7 @@
     try {
       ids = collectAccounts(progress);
     } catch (IOException e) {
-      log.error("Error collecting accounts", e);
+      logger.atSevere().withCause(e).log("Error collecting accounts");
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
     return reindexAccounts(index, ids, progress);
@@ -110,7 +109,7 @@
     try {
       Futures.successfulAsList(futures).get();
     } catch (ExecutionException | InterruptedException e) {
-      log.error("Error waiting on account futures", e);
+      logger.atSevere().withCause(e).log("Error waiting on account futures");
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
 
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 54bf0dc..babcba1 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -22,6 +22,7 @@
 
 import com.google.common.base.Stopwatch;
 import com.google.common.collect.ComparisonChain;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.index.SiteIndexer;
@@ -53,11 +54,9 @@
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
-  private static final Logger log = LoggerFactory.getLogger(AllChangesIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ChangeData.Factory changeDataFactory;
@@ -118,10 +117,10 @@
         changeCount += size;
         projects.add(new ProjectHolder(name, size));
       } catch (IOException e) {
-        log.error("Error collecting project {}", name, e);
+        logger.atSevere().withCause(e).log("Error collecting project %s", name);
         projectsFailed++;
         if (projectsFailed > projects.size() / 2) {
-          log.error("Over 50% of the projects could not be collected: aborted");
+          logger.atSevere().log("Over 50%% of the projects could not be collected: aborted");
           return new Result(sw, false, 0, 0);
         }
       }
@@ -176,7 +175,7 @@
               },
               directExecutor()));
     } catch (ExecutionException e) {
-      log.error("Error in batch indexer", e);
+      logger.atSevere().withCause(e).log("Error in batch indexer");
       ok.set(false);
     }
     // If too many changes failed, maybe there was a bug in the indexer. Don't
@@ -187,8 +186,8 @@
     int nTotal = nFailed + nDone;
     double pctFailed = ((double) nFailed) / nTotal * 100;
     if (pctFailed > 10) {
-      log.error(
-          "Failed {}/{} changes ({}%); not marking new index as ready",
+      logger.atSevere().log(
+          "Failed %s/%s changes (%s%%); not marking new index as ready",
           nFailed, nTotal, Math.round(pctFailed));
       ok.set(false);
     }
@@ -228,7 +227,7 @@
         // we don't have concrete proof that improving packfile locality would help.
         notesFactory.scan(repo, db, project).forEach(r -> index(db, r));
       } catch (RepositoryNotFoundException rnfe) {
-        log.error(rnfe.getMessage());
+        logger.atSevere().log(rnfe.getMessage());
       }
       return null;
     }
@@ -255,12 +254,7 @@
         this.failed.update(1);
       }
 
-      if (e != null) {
-        log.warn(error, e);
-      } else {
-        log.warn(error);
-      }
-
+      logger.atWarning().withCause(e).log(error);
       verboseWriter.println(error);
     }
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 82253f2..405e6fc 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -38,6 +38,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Longs;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
@@ -85,8 +86,6 @@
 import java.util.function.Function;
 import java.util.stream.Stream;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Fields indexed on change documents.
@@ -99,7 +98,7 @@
  * unambiguous derived field names containing other characters.
  */
 public class ChangeField {
-  private static final Logger log = LoggerFactory.getLogger(ChangeField.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final int NO_ASSIGNEE = -1;
 
@@ -267,7 +266,8 @@
 
       int i = v.indexOf(',');
       if (i < 0) {
-        log.warn("Invalid value for reviewer field from change {}: {}", changeId.get(), v);
+        logger.atWarning().log(
+            "Invalid value for reviewer field from change %s: %s", changeId.get(), v);
         continue;
       }
 
@@ -285,24 +285,23 @@
       com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
           Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
       if (!reviewerState.isPresent()) {
-        log.warn(
-            "Failed to parse reviewer state of reviewer field from change {}: {}",
-            changeId.get(),
-            v);
+        logger.atWarning().log(
+            "Failed to parse reviewer state of reviewer field from change %s: %s",
+            changeId.get(), v);
         continue;
       }
 
       Optional<Account.Id> accountId = Account.Id.tryParse(v.substring(i + 1, i2));
       if (!accountId.isPresent()) {
-        log.warn(
-            "Failed to parse account ID of reviewer field from change {}: {}", changeId.get(), v);
+        logger.atWarning().log(
+            "Failed to parse account ID of reviewer field from change %s: %s", changeId.get(), v);
         continue;
       }
 
       Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
       if (l == null) {
-        log.warn(
-            "Failed to parse timestamp of reviewer field from change {}: {}", changeId.get(), v);
+        logger.atWarning().log(
+            "Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
         continue;
       }
       Timestamp timestamp = new Timestamp(l);
@@ -318,7 +317,8 @@
     for (String v : values) {
       int i = v.indexOf(',');
       if (i < 0) {
-        log.warn("Invalid value for reviewer by email field from change {}: {}", changeId.get(), v);
+        logger.atWarning().log(
+            "Invalid value for reviewer by email field from change %s: %s", changeId.get(), v);
         continue;
       }
 
@@ -337,28 +337,25 @@
       com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
           Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
       if (!reviewerState.isPresent()) {
-        log.warn(
-            "Failed to parse reviewer state of reviewer by email field from change {}: {}",
-            changeId.get(),
-            v);
+        logger.atWarning().log(
+            "Failed to parse reviewer state of reviewer by email field from change %s: %s",
+            changeId.get(), v);
         continue;
       }
 
       Address address = Address.tryParse(v.substring(i + 1, i2));
       if (address == null) {
-        log.warn(
-            "Failed to parse address of reviewer by email field from change {}: {}",
-            changeId.get(),
-            v);
+        logger.atWarning().log(
+            "Failed to parse address of reviewer by email field from change %s: %s",
+            changeId.get(), v);
         continue;
       }
 
       Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
       if (l == null) {
-        log.warn(
-            "Failed to parse timestamp of reviewer by email field from change {}: {}",
-            changeId.get(),
-            v);
+        logger.atWarning().log(
+            "Failed to parse timestamp of reviewer by email field from change %s: %s",
+            changeId.get(), v);
         continue;
       }
       Timestamp timestamp = new Timestamp(l);
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 55f8b48..e947e60 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.extensions.events.EventUtil.logEventListenerError;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Atomics;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -56,8 +57,6 @@
 import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Helper for (re)indexing a change document.
@@ -66,7 +65,7 @@
  * fields and/or update the index.
  */
 public class ChangeIndexer {
-  private static final Logger log = LoggerFactory.getLogger(ChangeIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     ChangeIndexer create(ListeningExecutorService executor, ChangeIndex index);
@@ -184,15 +183,12 @@
   }
 
   /**
-   * Synchronously index a change.
+   * Synchronously index a change, then check if the index is stale due to a race condition.
    *
    * @param cd change to index.
    */
   public void index(ChangeData cd) throws IOException {
-    for (Index<?, ChangeData> i : getWriteIndexes()) {
-      i.replace(cd);
-    }
-    fireChangeIndexedEvent(cd.project().get(), cd.getId().get());
+    indexImpl(cd);
 
     // Always double-check whether the change might be stale immediately after
     // interactively indexing it. This fixes up the case where two writers write
@@ -215,6 +211,13 @@
     autoReindexIfStale(cd);
   }
 
+  private void indexImpl(ChangeData cd) throws IOException {
+    for (Index<?, ChangeData> i : getWriteIndexes()) {
+      i.replace(cd);
+    }
+    fireChangeIndexedEvent(cd.project().get(), cd.getId().get());
+  }
+
   private void fireChangeIndexedEvent(String projectName, int id) {
     for (ChangeIndexedListener listener : indexedListeners) {
       try {
@@ -243,8 +246,6 @@
    */
   public void index(ReviewDb db, Change change) throws IOException, OrmException {
     index(newChangeData(db, change));
-    // See comment in #index(ChangeData).
-    autoReindexIfStale(change.getProject(), change.getId());
   }
 
   /**
@@ -256,10 +257,7 @@
    */
   public void index(ReviewDb db, Project.NameKey project, Change.Id changeId)
       throws IOException, OrmException {
-    ChangeData cd = newChangeData(db, project, changeId);
-    index(cd);
-    // See comment in #index(ChangeData).
-    autoReindexIfStale(cd);
+    index(newChangeData(db, project, changeId));
   }
 
   /**
@@ -379,7 +377,7 @@
           }
         }
       } catch (Exception e) {
-        log.error("Failed to execute " + this, e);
+        logger.atSevere().withCause(e).log("Failed to execute %s", this);
         throw e;
       }
     }
@@ -419,7 +417,7 @@
       for (ChangeIndex i : getWriteIndexes()) {
         i.delete(id);
       }
-      log.info("Deleted change {} from index.", id.get());
+      logger.atInfo().log("Deleted change %s from index.", id.get());
       fireChangeDeletedFromIndexEvent(id.get());
       return null;
     }
@@ -434,19 +432,18 @@
     public Boolean callImpl(Provider<ReviewDb> db) throws Exception {
       try {
         if (stalenessChecker.isStale(id)) {
-          index(newChangeData(db.get(), project, id));
+          indexImpl(newChangeData(db.get(), project, id));
           return true;
         }
       } catch (NoSuchChangeException nsce) {
-        log.debug("Change {} was deleted, aborting reindexing the change.", id.get());
+        logger.atFine().log("Change %s was deleted, aborting reindexing the change.", id.get());
       } catch (Exception e) {
         if (!isCausedByRepositoryNotFoundException(e)) {
           throw e;
         }
-        log.debug(
-            "Change {} belongs to deleted project {}, aborting reindexing the change.",
-            id.get(),
-            project.get());
+        logger.atFine().log(
+            "Change %s belongs to deleted project %s, aborting reindexing the change.",
+            id.get(), project.get());
       }
       return false;
     }
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index 0b76b1e..609432b 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -17,6 +17,7 @@
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -47,11 +48,9 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
-  private static final Logger log = LoggerFactory.getLogger(ReindexAfterRefUpdate.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final OneOffRequestContext requestContext;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -97,7 +96,7 @@
           accountCache.evict(accountId);
           indexer.get().index(accountId);
         } catch (IOException e) {
-          log.error(String.format("Reindex account %s failed.", accountId), e);
+          logger.atSevere().withCause(e).log("Reindex account %s failed.", accountId);
         }
       }
     }
@@ -140,7 +139,7 @@
       try (ManualRequestContext ctx = requestContext.open()) {
         return impl(ctx);
       } catch (Exception e) {
-        log.error("Failed to reindex changes after " + event, e);
+        logger.atSevere().withCause(e).log("Failed to reindex changes after %s", event);
         throw e;
       }
     }
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index e7790df..208e949 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.reviewdb.client.Change;
@@ -49,12 +50,10 @@
 import java.util.regex.Pattern;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class StalenessChecker {
-  private static final Logger log = LoggerFactory.getLogger(StalenessChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final ImmutableSet<String> FIELDS =
       ImmutableSet.of(
@@ -200,7 +199,7 @@
       }
       return false;
     } catch (IOException e) {
-      log.warn(String.format("error checking staleness of %s in %s", id, project), e);
+      logger.atWarning().withCause(e).log("error checking staleness of %s in %s", id, project);
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index c90bece..2823c2e 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -42,12 +43,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, InternalGroup, GroupIndex> {
-  private static final Logger log = LoggerFactory.getLogger(AllGroupsIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ListeningExecutorService executor;
   private final GroupCache groupCache;
@@ -72,7 +71,7 @@
     try {
       uuids = collectGroups(progress);
     } catch (IOException | ConfigInvalidException e) {
-      log.error("Error collecting groups", e);
+      logger.atSevere().withCause(e).log("Error collecting groups");
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
     return reindexGroups(index, uuids, progress);
@@ -118,7 +117,7 @@
     try {
       Futures.successfulAsList(futures).get();
     } catch (ExecutionException | InterruptedException e) {
-      log.error("Error waiting on group futures", e);
+      logger.atSevere().withCause(e).log("Error waiting on group futures");
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
 
diff --git a/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java b/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
index 1e36f18..650df22 100644
--- a/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
+++ b/java/com/google/gerrit/server/index/project/AllProjectsIndexer.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -36,13 +37,10 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AllProjectsIndexer extends SiteIndexer<Project.NameKey, ProjectData, ProjectIndex> {
-
-  private static final Logger log = LoggerFactory.getLogger(AllProjectsIndexer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ListeningExecutorService executor;
   private final ProjectCache projectCache;
@@ -93,7 +91,7 @@
     try {
       Futures.successfulAsList(futures).get();
     } catch (ExecutionException | InterruptedException e) {
-      log.error("Error waiting on project futures", e);
+      logger.atSevere().withCause(e).log("Error waiting on project futures");
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
 
diff --git a/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
index 199731e..9032932 100644
--- a/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
+++ b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
@@ -14,16 +14,14 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.mail.receive.MailMessage;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Filters out auto-reply messages according to RFC 3834. */
 @Singleton
 public class AutoReplyMailFilter implements MailFilter {
-
-  private static final Logger log = LoggerFactory.getLogger(AutoReplyMailFilter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   public boolean shouldProcessMessage(MailMessage message) {
@@ -32,8 +30,8 @@
         String prec = header.substring(MailHeader.PRECEDENCE.fieldWithDelimiter().length()).trim();
 
         if (prec.equals("list") || prec.equals("junk") || prec.equals("bulk")) {
-          log.error(
-              "Message {} has a Precedence header. Will ignore and delete message.", message.id());
+          logger.atSevere().log(
+              "Message %s has a Precedence header. Will ignore and delete message.", message.id());
           return false;
         }
 
@@ -42,8 +40,8 @@
             header.substring(MailHeader.AUTO_SUBMITTED.fieldWithDelimiter().length()).trim();
 
         if (!autoSubmitted.equals("no")) {
-          log.error(
-              "Message {} has an Auto-Submitted header. Will ignore and delete message.",
+          logger.atSevere().log(
+              "Message %s has an Auto-Submitted header. Will ignore and delete message.",
               message.id());
           return false;
         }
diff --git a/java/com/google/gerrit/server/mail/ListMailFilter.java b/java/com/google/gerrit/server/mail/ListMailFilter.java
index 21347cb..5a41c77 100644
--- a/java/com/google/gerrit/server/mail/ListMailFilter.java
+++ b/java/com/google/gerrit/server/mail/ListMailFilter.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.joining;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.receive.MailMessage;
 import com.google.inject.Inject;
@@ -23,19 +24,17 @@
 import java.util.Arrays;
 import java.util.regex.Pattern;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ListMailFilter implements MailFilter {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public enum ListFilterMode {
     OFF,
     WHITELIST,
     BLACKLIST
   }
 
-  private static final Logger log = LoggerFactory.getLogger(ListMailFilter.class);
-
   private final ListFilterMode mode;
   private final Pattern mailPattern;
 
@@ -55,7 +54,7 @@
 
     boolean match = mailPattern.matcher(message.from().email).find();
     if (mode == ListFilterMode.WHITELIST && !match || mode == ListFilterMode.BLACKLIST && match) {
-      log.info("Mail message from " + message.from() + " rejected by list filter");
+      logger.atInfo().log("Mail message from %s rejected by list filter", message.from());
       return false;
     }
     return true;
diff --git a/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java b/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
index 6bb6211..169b41e 100644
--- a/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.receive;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gerrit.server.mail.Encryption;
@@ -24,12 +25,11 @@
 import java.util.List;
 import org.apache.commons.net.imap.IMAPClient;
 import org.apache.commons.net.imap.IMAPSClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ImapMailReceiver extends MailReceiver {
-  private static final Logger log = LoggerFactory.getLogger(ImapMailReceiver.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String INBOX_FOLDER = "INBOX";
 
   @Inject
@@ -71,7 +71,7 @@
         // should fetch.
         if (!imap.fetch("1:*", "(INTERNALDATE)")) {
           // false indicates that there are no messages to fetch
-          log.info("Fetched 0 messages via IMAP");
+          logger.atInfo().log("Fetched 0 messages via IMAP");
           return;
         }
         // Format of reply is one line per email and one line to indicate
@@ -81,7 +81,7 @@
         // * 2 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)")
         // AAAC OK FETCH completed.
         int numMessages = imap.getReplyStrings().length - 1;
-        log.info("Fetched " + numMessages + " messages via IMAP");
+        logger.atInfo().log("Fetched %d messages via IMAP", numMessages);
         // Fetch the full version of all emails
         List<MailMessage> mailMessages = new ArrayList<>(numMessages);
         for (int i = 1; i <= numMessages; i++) {
@@ -108,21 +108,22 @@
                 if (imap.store(i + ":" + i, "+FLAGS", "(\\Deleted)")) {
                   pendingDeletion.remove(mailMessage.id());
                 } else {
-                  log.error("Could not mark mail message as deleted: " + mailMessage.id());
+                  logger.atSevere().log(
+                      "Could not mark mail message as deleted: %s", mailMessage.id());
                 }
               } else {
                 mailMessages.add(mailMessage);
               }
             } catch (MailParsingException e) {
-              log.error("Exception while parsing email after IMAP fetch", e);
+              logger.atSevere().withCause(e).log("Exception while parsing email after IMAP fetch");
             }
           } else {
-            log.error("IMAP fetch failed. Will retry in next fetch cycle.");
+            logger.atSevere().log("IMAP fetch failed. Will retry in next fetch cycle.");
           }
         }
         // Permanently delete emails marked for deletion
         if (!imap.expunge()) {
-          log.error("Could not expunge IMAP emails");
+          logger.atSevere().log("Could not expunge IMAP emails");
         }
         dispatchMailProcessor(mailMessages, async);
       } finally {
diff --git a/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java b/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
index 05525bd..d176095 100644
--- a/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
+++ b/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
@@ -16,18 +16,17 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.MailUtil;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.time.format.DateTimeParseException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Parse metadata from inbound email */
 public class MailHeaderParser {
-  private static final Logger log = LoggerFactory.getLogger(MailHeaderParser.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static MailMetadata parse(MailMessage m) {
     MailMetadata metadata = new MailMetadata();
@@ -47,7 +46,8 @@
         try {
           metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
         } catch (DateTimeParseException e) {
-          log.error("Mail: Error while parsing timestamp from header of message " + m.id(), e);
+          logger.atSevere().withCause(e).log(
+              "Mail: Error while parsing timestamp from header of message %s", m.id());
         }
       } else if (header.startsWith(MailHeader.MESSAGE_TYPE.fieldWithDelimiter())) {
         metadata.messageType =
@@ -93,7 +93,8 @@
         try {
           metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
         } catch (DateTimeParseException e) {
-          log.error("Mail: Error while parsing timestamp from footer of message " + m.id(), e);
+          logger.atSevere().withCause(e).log(
+              "Mail: Error while parsing timestamp from footer of message %s", m.id());
         }
       } else if (metadata.messageType == null && line.contains(MailHeader.MESSAGE_TYPE.getName())) {
         metadata.messageType = extractFooter(MailHeader.MESSAGE_TYPE.withDelimiter(), line);
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 949cd82..0e0bca6 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.Side;
@@ -70,13 +71,11 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** A service that can attach the comments from a {@link MailMessage} to a change. */
 @Singleton
 public class MailProcessor {
-  private static final Logger log = LoggerFactory.getLogger(MailProcessor.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Emails emails;
   private final InboundEmailRejectionSender.Factory emailRejectionSender;
@@ -145,10 +144,9 @@
       throws OrmException, UpdateException, RestApiException, IOException {
     for (DynamicMap.Entry<MailFilter> filter : mailFilters) {
       if (!filter.getProvider().get().shouldProcessMessage(message)) {
-        log.warn(
-            String.format(
-                "Message %s filtered by plugin %s %s. Will delete message.",
-                message.id(), filter.getPluginName(), filter.getExportName()));
+        logger.atWarning().log(
+            "Message %s filtered by plugin %s %s. Will delete message.",
+            message.id(), filter.getPluginName(), filter.getExportName());
         return;
       }
     }
@@ -156,10 +154,9 @@
     MailMetadata metadata = MailHeaderParser.parse(message);
 
     if (!metadata.hasRequiredFields()) {
-      log.error(
-          String.format(
-              "Message %s is missing required metadata, have %s. Will delete message.",
-              message.id(), metadata));
+      logger.atSevere().log(
+          "Message %s is missing required metadata, have %s. Will delete message.",
+          message.id(), metadata);
       sendRejectionEmail(message, InboundEmailRejectionSender.Error.PARSING_ERROR);
       return;
     }
@@ -167,10 +164,10 @@
     Set<Account.Id> accountIds = emails.getAccountFor(metadata.author);
 
     if (accountIds.size() != 1) {
-      log.error(
-          String.format(
-              "Address %s could not be matched to a unique account. It was matched to %s. Will delete message.",
-              metadata.author, accountIds));
+      logger.atSevere().log(
+          "Address %s could not be matched to a unique account. It was matched to %s."
+              + " Will delete message.",
+          metadata.author, accountIds);
 
       // We don't want to send an email if no accounts are linked to it.
       if (accountIds.size() > 1) {
@@ -181,11 +178,11 @@
     Account.Id accountId = accountIds.iterator().next();
     Optional<AccountState> accountState = accountCache.get(accountId);
     if (!accountState.isPresent()) {
-      log.warn(String.format("Mail: Account %s doesn't exist. Will delete message.", accountId));
+      logger.atWarning().log("Mail: Account %s doesn't exist. Will delete message.", accountId);
       return;
     }
     if (!accountState.get().getAccount().isActive()) {
-      log.warn(String.format("Mail: Account %s is inactive. Will delete message.", accountId));
+      logger.atWarning().log("Mail: Account %s is inactive. Will delete message.", accountId);
       sendRejectionEmail(message, InboundEmailRejectionSender.Error.INACTIVE_ACCOUNT);
       return;
     }
@@ -199,7 +196,7 @@
           emailRejectionSender.create(message.from(), message.id(), reason);
       em.send();
     } catch (Exception e) {
-      log.error("Cannot send email to warn for an error", e);
+      logger.atSevere().withCause(e).log("Cannot send email to warn for an error");
     }
   }
 
@@ -210,18 +207,18 @@
       List<ChangeData> changeDataList =
           queryProvider.get().byLegacyChangeId(new Change.Id(metadata.changeNumber));
       if (changeDataList.size() != 1) {
-        log.error(
-            String.format(
-                "Message %s references unique change %s, but there are %d matching changes in the index. Will delete message.",
-                message.id(), metadata.changeNumber, changeDataList.size()));
+        logger.atSevere().log(
+            "Message %s references unique change %s,"
+                + " but there are %d matching changes in the index."
+                + " Will delete message.",
+            message.id(), metadata.changeNumber, changeDataList.size());
 
         sendRejectionEmail(message, InboundEmailRejectionSender.Error.INTERNAL_EXCEPTION);
         return;
       }
       ChangeData cd = changeDataList.get(0);
       if (existingMessageIds(cd).contains(message.id())) {
-        log.info(
-            String.format("Message %s was already processed. Will delete message.", message.id()));
+        logger.atInfo().log("Message %s was already processed. Will delete message.", message.id());
         return;
       }
       // Get all comments; filter and sort them to get the original list of
@@ -244,9 +241,8 @@
       }
 
       if (parsedComments.isEmpty()) {
-        log.warn(
-            String.format(
-                "Could not parse any comments from %s. Will delete message.", message.id()));
+        logger.atWarning().log(
+            "Could not parse any comments from %s. Will delete message.", message.id());
         sendRejectionEmail(message, InboundEmailRejectionSender.Error.PARSING_ERROR);
         return;
       }
diff --git a/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
index 6deb240..e4ad969 100644
--- a/java/com/google/gerrit/server/mail/receive/MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.receive;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -30,12 +31,10 @@
 import java.util.Timer;
 import java.util.TimerTask;
 import java.util.concurrent.Future;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** MailReceiver implements base functionality for receiving emails. */
 public abstract class MailReceiver implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(MailReceiver.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected EmailSettings mailSettings;
   protected Set<String> pendingDeletion;
@@ -91,7 +90,7 @@
             try {
               MailReceiver.this.handleEmails(true);
             } catch (MailTransferException | IOException e) {
-              log.error("Error while fetching emails", e);
+              logger.atSevere().withCause(e).log("Error while fetching emails");
             }
           }
         },
@@ -141,7 +140,8 @@
                         mailProcessor.process(m);
                         requestDeletion(m.id());
                       } catch (RestApiException | UpdateException e) {
-                        log.error("Mail: Can't process message " + m.id() + " . Won't delete.", e);
+                        logger.atSevere().withCause(e).log(
+                            "Mail: Can't process message %s . Won't delete.", m.id());
                       }
                     });
       } else {
@@ -150,7 +150,7 @@
           mailProcessor.process(m);
           requestDeletion(m.id());
         } catch (RestApiException | UpdateException e) {
-          log.error("Mail: Can't process messages. Won't delete.", e);
+          logger.atSevere().withCause(e).log("Mail: Can't process messages. Won't delete.");
         }
       }
     }
diff --git a/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java b/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
index bbb7e66..a3ea265 100644
--- a/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.receive;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSettings;
@@ -27,13 +28,11 @@
 import org.apache.commons.net.pop3.POP3Client;
 import org.apache.commons.net.pop3.POP3MessageInfo;
 import org.apache.commons.net.pop3.POP3SClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** An implementation of {@link MailReceiver} for POP3. */
 @Singleton
 public class Pop3MailReceiver extends MailReceiver {
-  private static final Logger log = LoggerFactory.getLogger(Pop3MailReceiver.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject
   Pop3MailReceiver(EmailSettings mailSettings, MailProcessor mailProcessor, WorkQueue workQueue) {
@@ -70,7 +69,7 @@
         if (messages == null) {
           throw new MailTransferException("Could not retrieve message list via POP3");
         }
-        log.info("Received " + messages.length + " messages via POP3");
+        logger.atInfo().log("Received %d messages via POP3", messages.length);
         // Fetch messages
         List<MailMessage> mailMessages = new ArrayList<>();
         for (POP3MessageInfo msginfo : messages) {
@@ -93,14 +92,14 @@
               if (pop3.deleteMessage(msginfo.number)) {
                 pendingDeletion.remove(mailMessage.id());
               } else {
-                log.error("Could not delete message " + msginfo.number);
+                logger.atSevere().log("Could not delete message %d", msginfo.number);
               }
             } else {
               // Process message further
               mailMessages.add(mailMessage);
             }
           } catch (MailParsingException e) {
-            log.error("Could not parse message " + msginfo.number);
+            logger.atSevere().log("Could not parse message %d", msginfo.number);
           }
         }
         dispatchMailProcessor(mailMessages, async);
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 11f50a9..503fbd0 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -62,12 +63,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Sends an email to one or more interested parties. */
 public abstract class ChangeEmail extends NotificationEmail {
-  private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected static ChangeData newChangeData(
       EmailArguments ea, Project.NameKey project, Change.Id id) {
@@ -300,7 +299,7 @@
       }
       return detail.toString();
     } catch (Exception err) {
-      log.warn("Cannot format change detail", err);
+      logger.atWarning().withCause(err).log("Cannot format change detail");
       return "";
     }
   }
@@ -376,7 +375,7 @@
         add(RecipientType.CC, id);
       }
     } catch (OrmException err) {
-      log.warn("Cannot CC users that reviewed updated change", err);
+      logger.atWarning().withCause(err).log("Cannot CC users that reviewed updated change");
     }
   }
 
@@ -391,7 +390,7 @@
         add(RecipientType.CC, id);
       }
     } catch (OrmException err) {
-      log.warn("Cannot CC users that commented on updated change", err);
+      logger.atWarning().withCause(err).log("Cannot CC users that commented on updated change");
     }
   }
 
@@ -514,7 +513,7 @@
         reviewers.add(getNameEmailFor(who));
       }
     } catch (OrmException e) {
-      log.warn("Cannot get change reviewers", e);
+      logger.atWarning().withCause(e).log("Cannot get change reviewers");
     }
     return reviewers;
   }
@@ -536,10 +535,10 @@
         return "[Octopus merge; cannot be formatted as a diff.]\n";
       }
     } catch (PatchListObjectTooLargeException e) {
-      log.warn("Cannot format patch " + e.getMessage());
+      logger.atWarning().log("Cannot format patch %s", e.getMessage());
       return "";
     } catch (PatchListNotAvailableException e) {
-      log.error("Cannot format patch", e);
+      logger.atSevere().withCause(e).log("Cannot format patch");
       return "";
     }
 
@@ -556,11 +555,11 @@
           if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
             return "";
           }
-          log.error("Cannot format patch", e);
+          logger.atSevere().withCause(e).log("Cannot format patch");
           return "";
         }
       } catch (IOException e) {
-        log.error("Cannot open repository to format patch", e);
+        logger.atSevere().withCause(e).log("Cannot open repository to format patch");
         return "";
       }
     }
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index b04dcd6..0095fc1 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.FilenameComparator;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
@@ -58,12 +59,10 @@
 import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Send comments, after the author of them hit used Publish Comments in the UI. */
 public class CommentSender extends ReplyToChangeSender {
-  private static final Logger log = LoggerFactory.getLogger(CommentSender.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     CommentSender create(Project.NameKey project, Change.Id id);
@@ -205,9 +204,9 @@
       try {
         patchList = getPatchList();
       } catch (PatchListObjectTooLargeException e) {
-        log.warn("Failed to get patch list: " + e.getMessage());
+        logger.atWarning().log("Failed to get patch list: %s", e.getMessage());
       } catch (PatchListNotAvailableException e) {
-        log.error("Failed to get patch list", e);
+        logger.atSevere().withCause(e).log("Failed to get patch list");
       }
     }
 
@@ -227,11 +226,9 @@
           try {
             currentGroup.fileData = new PatchFile(repo, patchList, c.key.filename);
           } catch (IOException e) {
-            log.warn(
-                String.format(
-                    "Cannot load %s from %s in %s",
-                    c.key.filename, patchList.getNewId().name(), projectState.getName()),
-                e);
+            logger.atWarning().withCause(e).log(
+                "Cannot load %s from %s in %s",
+                c.key.filename, patchList.getNewId().name(), projectState.getName());
             currentGroup.fileData = null;
           }
         }
@@ -321,7 +318,7 @@
     try {
       return commentsUtil.getPublished(args.db.get(), changeData.notes(), key);
     } catch (OrmException e) {
-      log.warn("Could not find the parent of this comment: " + child.toString());
+      logger.atWarning().log("Could not find the parent of this comment: %s", child);
       return Optional.empty();
     }
   }
@@ -539,16 +536,16 @@
       return fileInfo.getLine(side, lineNbr);
     } catch (IOException err) {
       // Default to the empty string if the file cannot be safely read.
-      log.warn(String.format("Failed to read file on side %d", side), err);
+      logger.atWarning().withCause(err).log("Failed to read file on side %d", side);
       return "";
     } catch (IndexOutOfBoundsException err) {
       // Default to the empty string if the given line number does not appear
       // in the file.
-      log.debug(String.format("Failed to get line number of file on side %d", side), err);
+      logger.atFine().withCause(err).log("Failed to get line number of file on side %d", side);
       return "";
     } catch (NoSuchEntityException err) {
       // Default to the empty string if the side cannot be found.
-      log.warn(String.format("Side %d of file didn't exist", side), err);
+      logger.atWarning().withCause(err).log("Side %d of file didn't exist", side);
       return "";
     }
   }
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index 156fde5..fc9c14a 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.reviewdb.client.Account;
@@ -27,12 +28,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.stream.StreamSupport;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Notify interested parties of a brand new change. */
 public class CreateChangeSender extends NewChangeSender {
-  private static final Logger log = LoggerFactory.getLogger(CreateChangeSender.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     CreateChangeSender create(Project.NameKey project, Change.Id id);
@@ -71,7 +70,7 @@
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
-      log.warn("Cannot notify watchers for new change", err);
+      logger.atWarning().withCause(err).log("Cannot notify watchers for new change");
     }
 
     includeWatchers(NotifyType.NEW_PATCHSETS, !change.isWorkInProgress() && !change.isPrivate());
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 2a24f38..0cc7a1d 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -16,6 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.reviewdb.client.Account;
@@ -27,12 +28,10 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.HashMap;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Common class for notifications that are related to a project and branch */
 public abstract class NotificationEmail extends OutgoingEmail {
-  private static final Logger log = LoggerFactory.getLogger(NotificationEmail.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected Branch.NameKey branch;
 
@@ -73,7 +72,7 @@
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
-      log.warn("Cannot BCC watchers for " + type, err);
+      logger.atWarning().withCause(err).log("Cannot BCC watchers for %s", type);
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index f1f1778..a62a910 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -51,12 +52,10 @@
 import java.util.StringJoiner;
 import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.util.SystemReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
-  private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected String messageClass;
   private final Set<Account.Id> rcptTo = new HashSet<>();
@@ -470,7 +469,7 @@
         add(rt, toAddress(to), override);
       }
     } catch (PermissionBackendException e) {
-      log.error("Error reading database for account: " + to, e);
+      logger.atSevere().withCause(e).log("Error reading database for account: %s", to);
     }
   }
 
@@ -491,9 +490,9 @@
   protected void add(RecipientType rt, Address addr, boolean override) {
     if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) {
       if (!args.validator.isValid(addr.getEmail())) {
-        log.warn("Not emailing " + addr.getEmail() + " (invalid email address)");
+        logger.atWarning().log("Not emailing %s (invalid email address)", addr.getEmail());
       } else if (!args.emailSender.canEmail(addr.getEmail())) {
-        log.warn("Not emailing " + addr.getEmail() + " (prohibited by allowrcpt)");
+        logger.atWarning().log("Not emailing %s (prohibited by allowrcpt)", addr.getEmail());
       } else {
         if (!smtpRcptTo.add(addr)) {
           if (!override) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java b/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
index 1a4d39b..bc6c89e 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
@@ -16,18 +16,17 @@
 
 import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.apache.commons.validator.routines.DomainValidator;
 import org.apache.commons.validator.routines.EmailValidator;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class OutgoingEmailValidator {
-  private static final Logger log = LoggerFactory.getLogger(OutgoingEmailValidator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject
   OutgoingEmailValidator(@GerritServerConfig Config config) {
@@ -38,7 +37,7 @@
       } catch (IllegalStateException e) {
         // Should only happen in tests, where the OutgoingEmailValidator
         // is instantiated repeatedly.
-        log.error("Failed to update TLD override: " + e.getMessage());
+        logger.atSevere().log("Failed to update TLD override: %s", e.getMessage());
       }
     }
   }
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 5b361f1..15197ef 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.index.query.Predicate;
@@ -40,11 +41,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ProjectWatch {
-  private static final Logger log = LoggerFactory.getLogger(ProjectWatch.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected final EmailArguments args;
   protected final ProjectState projectState;
@@ -102,12 +101,9 @@
           try {
             add(matching, nc);
           } catch (QueryParseException e) {
-            log.warn(
-                "Project {} has invalid notify {} filter \"{}\": {}",
-                state.getName(),
-                nc.getName(),
-                nc.getFilter(),
-                e.getMessage());
+            logger.atWarning().log(
+                "Project %s has invalid notify %s filter \"%s\": %s",
+                state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
           }
         }
       }
diff --git a/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java b/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java
index 7f0661c..1814e54 100644
--- a/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java
+++ b/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mime;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import eu.medsea.mimeutil.MimeException;
 import eu.medsea.mimeutil.MimeType;
 import eu.medsea.mimeutil.MimeUtil;
@@ -27,12 +28,11 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.Properties;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Loads mime types from {@code mime-types.properties} at specificity of 2. */
 public class DefaultFileExtensionRegistry extends MimeDetector {
-  private static final Logger log = LoggerFactory.getLogger(DefaultFileExtensionRegistry.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final ImmutableMap<String, MimeType> TYPES;
 
   static {
@@ -41,7 +41,7 @@
         DefaultFileExtensionRegistry.class.getResourceAsStream("mime-types.properties")) {
       prop.load(in);
     } catch (IOException e) {
-      log.warn("Cannot load mime-types.properties", e);
+      logger.atWarning().withCause(e).log("Cannot load mime-types.properties");
     }
 
     ImmutableMap.Builder<String, MimeType> b = ImmutableMap.builder();
diff --git a/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java b/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
index 7cb34e2..eecf935 100644
--- a/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
+++ b/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mime;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -29,14 +30,13 @@
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class MimeUtilFileTypeRegistry implements FileTypeRegistry {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String KEY_SAFE = "safe";
   private static final String SECTION_MIMETYPE = "mimetype";
-  private static final Logger log = LoggerFactory.getLogger(MimeUtilFileTypeRegistry.class);
 
   private final Config cfg;
   private final MimeUtil2 mimeUtil;
@@ -85,7 +85,7 @@
       try {
         mimeTypes.addAll(mimeUtil.getMimeTypes(content));
       } catch (MimeException e) {
-        log.warn("Unable to determine MIME type from content", e);
+        logger.atWarning().withCause(e).log("Unable to determine MIME type from content");
       }
     }
     return getMimeType(mimeTypes, path);
@@ -98,7 +98,7 @@
     try {
       mimeTypes.addAll(mimeUtil.getMimeTypes(is));
     } catch (MimeException e) {
-      log.warn("Unable to determine MIME type from content", e);
+      logger.atWarning().withCause(e).log("Unable to determine MIME type from content");
     }
     return getMimeType(mimeTypes, path);
   }
@@ -108,7 +108,7 @@
     try {
       mimeTypes.addAll(mimeUtil.getMimeTypes(path));
     } catch (MimeException e) {
-      log.warn("Unable to determine MIME type from path", e);
+      logger.atWarning().withCause(e).log("Unable to determine MIME type from path");
     }
 
     if (isUnknownType(mimeTypes)) {
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index ef2c9b3..a083a71 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -48,7 +48,8 @@
     final GitRepositoryManager repoManager;
     final NotesMigration migration;
     final AllUsersName allUsers;
-    final ChangeNoteUtil noteUtil;
+    final ChangeNoteJson changeNoteJson;
+    final LegacyChangeNoteRead legacyChangeNoteRead;
     final NoteDbMetrics metrics;
     final Provider<ReviewDb> db;
 
@@ -65,7 +66,8 @@
         GitRepositoryManager repoManager,
         NotesMigration migration,
         AllUsersName allUsers,
-        ChangeNoteUtil noteUtil,
+        ChangeNoteJson changeNoteJson,
+        LegacyChangeNoteRead legacyChangeNoteRead,
         NoteDbMetrics metrics,
         Provider<ReviewDb> db,
         Provider<ChangeRebuilder> rebuilder,
@@ -73,7 +75,8 @@
       this.repoManager = repoManager;
       this.migration = migration;
       this.allUsers = allUsers;
-      this.noteUtil = noteUtil;
+      this.legacyChangeNoteRead = legacyChangeNoteRead;
+      this.changeNoteJson = changeNoteJson;
       this.metrics = metrics;
       this.db = db;
       this.rebuilder = rebuilder;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 010c5c0..3653bc7 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -120,7 +120,9 @@
       ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Date when) {
     checkUserType(u);
     if (u instanceof IdentifiedUser) {
-      return noteUtil.newIdent(u.asIdentifiedUser().getAccount(), when, serverIdent);
+      return noteUtil
+          .getLegacyChangeNoteWrite()
+          .newIdent(u.asIdentifiedUser().getAccount(), when, serverIdent);
     } else if (u instanceof InternalUser) {
       return serverIdent;
     }
@@ -175,7 +177,7 @@
   }
 
   protected PersonIdent newIdent(Account.Id authorId, Date when) {
-    return noteUtil.newIdent(authorId, when, serverIdent);
+    return noteUtil.getLegacyChangeNoteWrite().newIdent(authorId, when, serverIdent);
   }
 
   /** Whether no updates have been done. */
diff --git a/java/com/google/gerrit/server/notedb/ChangeBundle.java b/java/com/google/gerrit/server/notedb/ChangeBundle.java
index 7714c6e..1d3c752 100644
--- a/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -505,11 +505,11 @@
     if (rn >= 0) {
       s = s.substring(0, rn);
     }
-    return ChangeNoteUtil.sanitizeFooter(s);
+    return NoteDbUtil.sanitizeFooter(s);
   }
 
   private static String cleanNoteDbSubject(String s) {
-    return ChangeNoteUtil.sanitizeFooter(s);
+    return NoteDbUtil.sanitizeFooter(s);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 71c0b9e..6b4bea7 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -178,7 +178,12 @@
     for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
       updatedRevs.add(e.getKey());
       ObjectId id = ObjectId.fromString(e.getKey().get());
-      byte[] data = e.getValue().build(noteUtil, noteUtil.getWriteJson());
+      byte[] data =
+          e.getValue()
+              .build(
+                  noteUtil.getChangeNoteJson(),
+                  noteUtil.getLegacyChangeNoteWrite(),
+                  noteUtil.getChangeNoteJson().getWriteJson());
       if (!Arrays.equals(data, e.getValue().baseRaw)) {
         touchedAnyRevs = true;
       }
@@ -236,7 +241,12 @@
     // Even though reading from changes might not be enabled, we need to
     // parse any existing revision notes so we can merge them.
     return RevisionNoteMap.parse(
-        noteUtil, getId(), rw.getObjectReader(), noteMap, PatchLineComment.Status.DRAFT);
+        noteUtil.getChangeNoteJson(),
+        noteUtil.getLegacyChangeNoteRead(),
+        getId(),
+        rw.getObjectReader(),
+        noteMap,
+        PatchLineComment.Status.DRAFT);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
new file mode 100644
index 0000000..0475fe3
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
@@ -0,0 +1,49 @@
+// 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.notedb;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class ChangeNoteJson {
+  private final Gson gson = newGson();
+  private final boolean writeJson;
+
+  static Gson newGson() {
+    return new GsonBuilder()
+        .registerTypeAdapter(Timestamp.class, new CommentTimestampAdapter().nullSafe())
+        .setPrettyPrinting()
+        .create();
+  }
+
+  public Gson getGson() {
+    return gson;
+  }
+
+  public boolean getWriteJson() {
+    return writeJson;
+  }
+
+  @Inject
+  ChangeNoteJson(@GerritServerConfig Config config) {
+    this.writeJson = config.getBoolean("notedb", "writeJson", true);
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index e8e4dec..070f974 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -14,52 +14,8 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
-import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.CharMatcher;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Comment;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 import com.google.inject.Inject;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.sql.Timestamp;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Optional;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.util.GitDateFormatter;
-import org.eclipse.jgit.util.GitDateFormatter.Format;
-import org.eclipse.jgit.util.GitDateParser;
-import org.eclipse.jgit.util.MutableInteger;
-import org.eclipse.jgit.util.QuotedString;
-import org.eclipse.jgit.util.RawParseUtils;
 
 public class ChangeNoteUtil {
   public static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
@@ -85,560 +41,43 @@
   public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
   public static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
 
-  private static final String AUTHOR = "Author";
-  private static final String BASE_PATCH_SET = "Base-for-patch-set";
-  private static final String COMMENT_RANGE = "Comment-range";
-  private static final String FILE = "File";
-  private static final String LENGTH = "Bytes";
-  private static final String PARENT = "Parent";
-  private static final String PARENT_NUMBER = "Parent-number";
-  private static final String PATCH_SET = "Patch-set";
-  private static final String REAL_AUTHOR = "Real-author";
-  private static final String REVISION = "Revision";
-  private static final String UUID = "UUID";
-  private static final String UNRESOLVED = "Unresolved";
-  private static final String TAG = FOOTER_TAG.getName();
+  static final String AUTHOR = "Author";
+  static final String BASE_PATCH_SET = "Base-for-patch-set";
+  static final String COMMENT_RANGE = "Comment-range";
+  static final String FILE = "File";
+  static final String LENGTH = "Bytes";
+  static final String PARENT = "Parent";
+  static final String PARENT_NUMBER = "Parent-number";
+  static final String PATCH_SET = "Patch-set";
+  static final String REAL_AUTHOR = "Real-author";
+  static final String REVISION = "Revision";
+  static final String UUID = "UUID";
+  static final String UNRESOLVED = "Unresolved";
+  static final String TAG = FOOTER_TAG.getName();
 
-  public static String formatTime(PersonIdent ident, Timestamp t) {
-    GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
-    // TODO(dborowitz): Use a ThreadLocal or use Joda.
-    PersonIdent newIdent = new PersonIdent(ident, t);
-    return dateFormatter.formatDate(newIdent);
-  }
-
-  static Gson newGson() {
-    return new GsonBuilder()
-        .registerTypeAdapter(Timestamp.class, new CommentTimestampAdapter().nullSafe())
-        .setPrettyPrinting()
-        .create();
-  }
-
-  private final AccountCache accountCache;
-  private final PersonIdent serverIdent;
-  private final String serverId;
-  private final Gson gson = newGson();
-  private final boolean writeJson;
+  private final LegacyChangeNoteRead legacyChangeNoteRead;
+  private final LegacyChangeNoteWrite legacyChangeNoteWrite;
+  private final ChangeNoteJson changeNoteJson;
 
   @Inject
   public ChangeNoteUtil(
-      AccountCache accountCache,
-      @GerritPersonIdent PersonIdent serverIdent,
-      @GerritServerId String serverId,
-      @GerritServerConfig Config config) {
-    this.accountCache = accountCache;
-    this.serverIdent = serverIdent;
-    this.serverId = serverId;
-    this.writeJson = config.getBoolean("notedb", "writeJson", true);
+      ChangeNoteJson changeNoteJson,
+      LegacyChangeNoteRead legacyChangeNoteRead,
+      LegacyChangeNoteWrite legacyChangeNoteWrite) {
+    this.changeNoteJson = changeNoteJson;
+    this.legacyChangeNoteRead = legacyChangeNoteRead;
+    this.legacyChangeNoteWrite = legacyChangeNoteWrite;
   }
 
-  public PersonIdent newIdent(Account.Id authorId, Date when, PersonIdent serverIdent) {
-    Optional<Account> author = accountCache.get(authorId).map(AccountState::getAccount);
-    return new PersonIdent(
-        author.map(Account::getName).orElseGet(() -> Account.getName(authorId)),
-        authorId.get() + "@" + serverId,
-        when,
-        serverIdent.getTimeZone());
+  public LegacyChangeNoteRead getLegacyChangeNoteRead() {
+    return legacyChangeNoteRead;
   }
 
-  @VisibleForTesting
-  public PersonIdent newIdent(Account author, Date when, PersonIdent serverIdent) {
-    return new PersonIdent(
-        author.getName(), author.getId().get() + "@" + serverId, when, serverIdent.getTimeZone());
+  public ChangeNoteJson getChangeNoteJson() {
+    return changeNoteJson;
   }
 
-  public boolean getWriteJson() {
-    return writeJson;
-  }
-
-  public Gson getGson() {
-    return gson;
-  }
-
-  public String getServerId() {
-    return serverId;
-  }
-
-  public Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
-      throws ConfigInvalidException {
-    return NoteDbUtil.parseIdent(ident, serverId)
-        .orElseThrow(
-            () ->
-                parseException(
-                    changeId,
-                    "invalid identity, expected <id>@%s: %s",
-                    serverId,
-                    ident.getEmailAddress()));
-  }
-
-  private static boolean match(byte[] note, MutableInteger p, byte[] expected) {
-    int m = RawParseUtils.match(note, p.value, expected);
-    return m == p.value + expected.length;
-  }
-
-  public List<Comment> parseNote(byte[] note, MutableInteger p, Change.Id changeId)
-      throws ConfigInvalidException {
-    if (p.value >= note.length) {
-      return ImmutableList.of();
-    }
-    Set<Comment.Key> seen = new HashSet<>();
-    List<Comment> result = new ArrayList<>();
-    int sizeOfNote = note.length;
-    byte[] psb = PATCH_SET.getBytes(UTF_8);
-    byte[] bpsb = BASE_PATCH_SET.getBytes(UTF_8);
-    byte[] bpn = PARENT_NUMBER.getBytes(UTF_8);
-
-    RevId revId = new RevId(parseStringField(note, p, changeId, REVISION));
-    String fileName = null;
-    PatchSet.Id psId = null;
-    boolean isForBase = false;
-    Integer parentNumber = null;
-
-    while (p.value < sizeOfNote) {
-      boolean matchPs = match(note, p, psb);
-      boolean matchBase = match(note, p, bpsb);
-      if (matchPs) {
-        fileName = null;
-        psId = parsePsId(note, p, changeId, PATCH_SET);
-        isForBase = false;
-      } else if (matchBase) {
-        fileName = null;
-        psId = parsePsId(note, p, changeId, BASE_PATCH_SET);
-        isForBase = true;
-        if (match(note, p, bpn)) {
-          parentNumber = parseParentNumber(note, p, changeId);
-        }
-      } else if (psId == null) {
-        throw parseException(changeId, "missing %s or %s header", PATCH_SET, BASE_PATCH_SET);
-      }
-
-      Comment c = parseComment(note, p, fileName, psId, revId, isForBase, parentNumber);
-      fileName = c.key.filename;
-      if (!seen.add(c.key)) {
-        throw parseException(changeId, "multiple comments for %s in note", c.key);
-      }
-      result.add(c);
-    }
-    return result;
-  }
-
-  private Comment parseComment(
-      byte[] note,
-      MutableInteger curr,
-      String currentFileName,
-      PatchSet.Id psId,
-      RevId revId,
-      boolean isForBase,
-      Integer parentNumber)
-      throws ConfigInvalidException {
-    Change.Id changeId = psId.getParentKey();
-
-    // Check if there is a new file.
-    boolean newFile = (RawParseUtils.match(note, curr.value, FILE.getBytes(UTF_8))) != -1;
-    if (newFile) {
-      // If so, parse the new file name.
-      currentFileName = parseFilename(note, curr, changeId);
-    } else if (currentFileName == null) {
-      throw parseException(changeId, "could not parse %s", FILE);
-    }
-
-    CommentRange range = parseCommentRange(note, curr);
-    if (range == null) {
-      throw parseException(changeId, "could not parse %s", COMMENT_RANGE);
-    }
-
-    Timestamp commentTime = parseTimestamp(note, curr, changeId);
-    Account.Id aId = parseAuthor(note, curr, changeId, AUTHOR);
-    boolean hasRealAuthor =
-        (RawParseUtils.match(note, curr.value, REAL_AUTHOR.getBytes(UTF_8))) != -1;
-    Account.Id raId = null;
-    if (hasRealAuthor) {
-      raId = parseAuthor(note, curr, changeId, REAL_AUTHOR);
-    }
-
-    boolean hasParent = (RawParseUtils.match(note, curr.value, PARENT.getBytes(UTF_8))) != -1;
-    String parentUUID = null;
-    boolean unresolved = false;
-    if (hasParent) {
-      parentUUID = parseStringField(note, curr, changeId, PARENT);
-    }
-    boolean hasUnresolved =
-        (RawParseUtils.match(note, curr.value, UNRESOLVED.getBytes(UTF_8))) != -1;
-    if (hasUnresolved) {
-      unresolved = parseBooleanField(note, curr, changeId, UNRESOLVED);
-    }
-
-    String uuid = parseStringField(note, curr, changeId, UUID);
-
-    boolean hasTag = (RawParseUtils.match(note, curr.value, TAG.getBytes(UTF_8))) != -1;
-    String tag = null;
-    if (hasTag) {
-      tag = parseStringField(note, curr, changeId, TAG);
-    }
-
-    int commentLength = parseCommentLength(note, curr, changeId);
-
-    String message = RawParseUtils.decode(UTF_8, note, curr.value, curr.value + commentLength);
-    checkResult(message, "message contents", changeId);
-
-    Comment c =
-        new Comment(
-            new Comment.Key(uuid, currentFileName, psId.get()),
-            aId,
-            commentTime,
-            isForBase ? (short) (parentNumber == null ? 0 : -parentNumber) : (short) 1,
-            message,
-            serverId,
-            unresolved);
-    c.lineNbr = range.getEndLine();
-    c.parentUuid = parentUUID;
-    c.tag = tag;
-    c.setRevId(revId);
-    if (raId != null) {
-      c.setRealAuthor(raId);
-    }
-
-    if (range.getStartCharacter() != -1) {
-      c.setRange(range);
-    }
-
-    curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
-    curr.value = RawParseUtils.nextLF(note, curr.value);
-    return c;
-  }
-
-  private static String parseStringField(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    checkHeaderLineFormat(note, curr, fieldName, changeId);
-    int startOfField = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
-    curr.value = endOfLine;
-    return RawParseUtils.decode(UTF_8, note, startOfField, endOfLine - 1);
-  }
-
-  /**
-   * @return a comment range. If the comment range line in the note only has one number, we return a
-   *     CommentRange with that one number as the end line and the other fields as -1. If the
-   *     comment range line in the note contains a whole comment range, then we return a
-   *     CommentRange with all fields set. If the line is not correctly formatted, return null.
-   */
-  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
-    CommentRange range = new CommentRange(-1, -1, -1, -1);
-
-    int last = ptr.value;
-    int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == '\n') {
-      range.setEndLine(startLine);
-      ptr.value += 1;
-      return range;
-    } else if (note[ptr.value] == ':') {
-      range.setStartLine(startLine);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-
-    last = ptr.value;
-    int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == '-') {
-      range.setStartCharacter(startChar);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-
-    last = ptr.value;
-    int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == ':') {
-      range.setEndLine(endLine);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-
-    last = ptr.value;
-    int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
-    if (ptr.value == last) {
-      return null;
-    } else if (note[ptr.value] == '\n') {
-      range.setEndCharacter(endChar);
-      ptr.value += 1;
-    } else {
-      return null;
-    }
-    return range;
-  }
-
-  private static PatchSet.Id parsePsId(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, fieldName, changeId);
-    int startOfPsId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
-    MutableInteger i = new MutableInteger();
-    int patchSetId = RawParseUtils.parseBase10(note, startOfPsId, i);
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine - 1) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-    checkResult(patchSetId, "patchset id", changeId);
-    curr.value = endOfLine;
-    return new PatchSet.Id(changeId, patchSetId);
-  }
-
-  private static Integer parseParentNumber(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, PARENT_NUMBER, changeId);
-
-    int start = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
-    MutableInteger i = new MutableInteger();
-    int parentNumber = RawParseUtils.parseBase10(note, start, i);
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine - 1) {
-      throw parseException(changeId, "could not parse %s", PARENT_NUMBER);
-    }
-    checkResult(parentNumber, "parent number", changeId);
-    curr.value = endOfLine;
-    return Integer.valueOf(parentNumber);
-  }
-
-  private static String parseFilename(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, FILE, changeId);
-    int startOfFileName = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    curr.value = endOfLine;
-    curr.value = RawParseUtils.nextLF(note, curr.value);
-    return QuotedString.GIT_PATH.dequote(
-        RawParseUtils.decode(UTF_8, note, startOfFileName, endOfLine - 1));
-  }
-
-  private static Timestamp parseTimestamp(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    Timestamp commentTime;
-    String dateString = RawParseUtils.decode(UTF_8, note, curr.value, endOfLine - 1);
-    try {
-      commentTime = new Timestamp(GitDateParser.parse(dateString, null, Locale.US).getTime());
-    } catch (ParseException e) {
-      throw new ConfigInvalidException("could not parse comment timestamp", e);
-    }
-    curr.value = endOfLine;
-    return checkResult(commentTime, "comment timestamp", changeId);
-  }
-
-  private Account.Id parseAuthor(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, fieldName, changeId);
-    int startOfAccountId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
-    PersonIdent ident = RawParseUtils.parsePersonIdent(note, startOfAccountId);
-    Account.Id aId = parseIdent(ident, changeId);
-    curr.value = RawParseUtils.nextLF(note, curr.value);
-    return checkResult(aId, fieldName, changeId);
-  }
-
-  private static int parseCommentLength(byte[] note, MutableInteger curr, Change.Id changeId)
-      throws ConfigInvalidException {
-    checkHeaderLineFormat(note, curr, LENGTH, changeId);
-    int startOfLength = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
-    MutableInteger i = new MutableInteger();
-    i.value = startOfLength;
-    int commentLength = RawParseUtils.parseBase10(note, startOfLength, i);
-    if (i.value == startOfLength) {
-      throw parseException(changeId, "could not parse %s", LENGTH);
-    }
-    int endOfLine = RawParseUtils.nextLF(note, curr.value);
-    if (i.value != endOfLine - 1) {
-      throw parseException(changeId, "could not parse %s", LENGTH);
-    }
-    curr.value = endOfLine;
-    return checkResult(commentLength, "comment length", changeId);
-  }
-
-  private boolean parseBooleanField(
-      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
-      throws ConfigInvalidException {
-    String str = parseStringField(note, curr, changeId, fieldName);
-    if ("true".equalsIgnoreCase(str)) {
-      return true;
-    } else if ("false".equalsIgnoreCase(str)) {
-      return false;
-    }
-    throw parseException(changeId, "invalid boolean for %s: %s", fieldName, str);
-  }
-
-  private static <T> T checkResult(T o, String fieldName, Change.Id changeId)
-      throws ConfigInvalidException {
-    if (o == null) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-    return o;
-  }
-
-  private static int checkResult(int i, String fieldName, Change.Id changeId)
-      throws ConfigInvalidException {
-    if (i <= 0) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-    return i;
-  }
-
-  private void appendHeaderField(PrintWriter writer, String field, String value) {
-    writer.print(field);
-    writer.print(": ");
-    writer.print(value);
-    writer.print('\n');
-  }
-
-  private static void checkHeaderLineFormat(
-      byte[] note, MutableInteger curr, String fieldName, Change.Id changeId)
-      throws ConfigInvalidException {
-    boolean correct = RawParseUtils.match(note, curr.value, fieldName.getBytes(UTF_8)) != -1;
-    int p = curr.value + fieldName.length();
-    correct &= (p < note.length && note[p] == ':');
-    p++;
-    correct &= (p < note.length && note[p] == ' ');
-    if (!correct) {
-      throw parseException(changeId, "could not parse %s", fieldName);
-    }
-  }
-
-  /**
-   * Build a note that contains the metadata for and the contents of all of the comments in the
-   * given comments.
-   *
-   * @param comments Comments to be written to the output stream, keyed by patch set ID; multiple
-   *     patch sets are allowed since base revisions may be shared across patch sets. All of the
-   *     comments must share the same RevId, and all the comments for a given patch set must have
-   *     the same side.
-   * @param out output stream to write to.
-   */
-  void buildNote(ListMultimap<Integer, Comment> comments, OutputStream out) {
-    if (comments.isEmpty()) {
-      return;
-    }
-
-    List<Integer> psIds = new ArrayList<>(comments.keySet());
-    Collections.sort(psIds);
-
-    OutputStreamWriter streamWriter = new OutputStreamWriter(out, UTF_8);
-    try (PrintWriter writer = new PrintWriter(streamWriter)) {
-      String revId = comments.values().iterator().next().revId;
-      appendHeaderField(writer, REVISION, revId);
-
-      for (int psId : psIds) {
-        List<Comment> psComments = COMMENT_ORDER.sortedCopy(comments.get(psId));
-        Comment first = psComments.get(0);
-
-        short side = first.side;
-        appendHeaderField(writer, side <= 0 ? BASE_PATCH_SET : PATCH_SET, Integer.toString(psId));
-        if (side < 0) {
-          appendHeaderField(writer, PARENT_NUMBER, Integer.toString(-side));
-        }
-
-        String currentFilename = null;
-
-        for (Comment c : psComments) {
-          checkArgument(
-              revId.equals(c.revId),
-              "All comments being added must have all the same RevId. The "
-                  + "comment below does not have the same RevId as the others "
-                  + "(%s).\n%s",
-              revId,
-              c);
-          checkArgument(
-              side == c.side,
-              "All comments being added must all have the same side. The "
-                  + "comment below does not have the same side as the others "
-                  + "(%s).\n%s",
-              side,
-              c);
-          String commentFilename = QuotedString.GIT_PATH.quote(c.key.filename);
-
-          if (!commentFilename.equals(currentFilename)) {
-            currentFilename = commentFilename;
-            writer.print("File: ");
-            writer.print(commentFilename);
-            writer.print("\n\n");
-          }
-
-          appendOneComment(writer, c);
-        }
-      }
-    }
-  }
-
-  private void appendOneComment(PrintWriter writer, Comment c) {
-    // The CommentRange field for a comment is allowed to be null. If it is
-    // null, then in the first line, we simply use the line number field for a
-    // comment instead. If it isn't null, we write the comment range itself.
-    Comment.Range range = c.range;
-    if (range != null) {
-      writer.print(range.startLine);
-      writer.print(':');
-      writer.print(range.startChar);
-      writer.print('-');
-      writer.print(range.endLine);
-      writer.print(':');
-      writer.print(range.endChar);
-    } else {
-      writer.print(c.lineNbr);
-    }
-    writer.print("\n");
-
-    writer.print(formatTime(serverIdent, c.writtenOn));
-    writer.print("\n");
-
-    appendIdent(writer, AUTHOR, c.author.getId(), c.writtenOn);
-    if (!c.getRealAuthor().equals(c.author)) {
-      appendIdent(writer, REAL_AUTHOR, c.getRealAuthor().getId(), c.writtenOn);
-    }
-
-    String parent = c.parentUuid;
-    if (parent != null) {
-      appendHeaderField(writer, PARENT, parent);
-    }
-
-    appendHeaderField(writer, UNRESOLVED, Boolean.toString(c.unresolved));
-    appendHeaderField(writer, UUID, c.key.uuid);
-
-    if (c.tag != null) {
-      appendHeaderField(writer, TAG, c.tag);
-    }
-
-    byte[] messageBytes = c.message.getBytes(UTF_8);
-    appendHeaderField(writer, LENGTH, Integer.toString(messageBytes.length));
-
-    writer.print(c.message);
-    writer.print("\n\n");
-  }
-
-  private void appendIdent(PrintWriter writer, String header, Account.Id id, Timestamp ts) {
-    PersonIdent ident = newIdent(id, ts, serverIdent);
-    StringBuilder name = new StringBuilder();
-    PersonIdent.appendSanitized(name, ident.getName());
-    name.append(" <");
-    PersonIdent.appendSanitized(name, ident.getEmailAddress());
-    name.append('>');
-    appendHeaderField(writer, header, name.toString());
-  }
-
-  private static final CharMatcher INVALID_FOOTER_CHARS = CharMatcher.anyOf("\r\n\0");
-
-  static String sanitizeFooter(String value) {
-    // Remove characters that would confuse JGit's footer parser if they were
-    // included in footer values, for example by splitting the footer block into
-    // multiple paragraphs.
-    //
-    // One painful example: RevCommit#getShorMessage() might return a message
-    // containing "\r\r", which RevCommit#getFooterLines() will treat as an
-    // empty paragraph for the purposes of footer parsing.
-    return INVALID_FOOTER_CHARS.trimAndCollapseFrom(value, ' ');
+  public LegacyChangeNoteWrite getLegacyChangeNoteWrite() {
+    return legacyChangeNoteWrite;
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 1bbecc8..7e66d929 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -36,6 +36,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.metrics.Timer1;
@@ -83,12 +84,10 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
 public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
-  private static final Logger log = LoggerFactory.getLogger(ChangeNotes.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final Ordering<PatchSetApproval> PSA_BY_TIME =
       Ordering.from(comparing(PatchSetApproval::getGranted));
@@ -148,7 +147,7 @@
         throw new NoSuchChangeException(changeId);
       }
       if (changes.size() != 1) {
-        log.error(String.format("Multiple changes found for %d", changeId.get()));
+        logger.atSevere().log("Multiple changes found for %d", changeId.get());
         throw new NoSuchChangeException(changeId);
       }
       return changes.get(0).notes();
@@ -365,20 +364,19 @@
         if (defaultStorage == PrimaryStorage.REVIEW_DB) {
           // If changes should exist in ReviewDb, it's worth warning about a meta ref with
           // no corresponding ReviewDb data.
-          log.warn("skipping change {} found in project {} but not in ReviewDb", id, project);
+          logger.atWarning().log(
+              "skipping change %s found in project %s but not in ReviewDb", id, project);
           return null;
         }
         // TODO(dborowitz): See discussion in NoteDbBatchUpdate#newChangeContext.
         change = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
       } else if (!change.getProject().equals(project)) {
-        log.error(
-            "skipping change {} found in project {} because ReviewDb change has" + " project {}",
-            id,
-            project,
-            change.getProject());
+        logger.atSevere().log(
+            "skipping change %s found in project %s because ReviewDb change has project %s",
+            id, project, change.getProject());
         return null;
       }
-      log.debug("adding change {} found in project {}", id, project);
+      logger.atFine().log("adding change %s found in project %s", id, project);
       return toResult(change);
     }
 
@@ -761,7 +759,7 @@
           //
           // Parse notes from the staged result so we can return something useful
           // to the caller instead of throwing.
-          log.debug("Rebuilding change {} failed: {}", getChangeId(), e.getMessage());
+          logger.atFine().log("Rebuilding change %s failed: %s", getChangeId(), e.getMessage());
           args.metrics.autoRebuildFailureCount.increment(CHANGES);
           rebuildResult = checkNotNull(r);
           checkNotNull(r.newState());
@@ -778,8 +776,8 @@
     } catch (OrmException e) {
       throw new IOException(e);
     } finally {
-      log.debug(
-          "Rebuilt change {} in project {} in {} ms",
+      logger.atFine().log(
+          "Rebuilt change %s in project %s in %s ms",
           getChangeId(),
           getProjectName(),
           TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index d1c28c4..0bf2108 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -34,7 +35,6 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -42,7 +42,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
@@ -83,28 +82,22 @@
 
       @Override
       public byte[] serialize(Key object) {
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        object.id().copyRawTo(buf, 0);
         return ProtoCacheSerializers.toByteArray(
             ChangeNotesKeyProto.newBuilder()
                 .setProject(object.project().get())
                 .setChangeId(object.changeId().get())
-                .setId(ByteString.copyFrom(buf))
+                .setId(ObjectIdConverter.create().toByteString(object.id()))
                 .build());
       }
 
       @Override
       public Key deserialize(byte[] in) {
-        ChangeNotesKeyProto proto;
-        try {
-          proto = ChangeNotesKeyProto.parseFrom(in);
-        } catch (IOException e) {
-          throw new IllegalArgumentException("Failed to deserialize " + Key.class.getName());
-        }
+        ChangeNotesKeyProto proto =
+            ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), in);
         return Key.create(
             new Project.NameKey(proto.getProject()),
             new Change.Id(proto.getChangeId()),
-            ObjectId.fromRaw(proto.getId().toByteArray()));
+            ObjectIdConverter.create().fromByteString(proto.getId()));
       }
     }
   }
@@ -353,7 +346,13 @@
     @Override
     public ChangeNotesState call() throws ConfigInvalidException, IOException {
       ChangeNotesParser parser =
-          new ChangeNotesParser(key.changeId(), key.id(), rw, args.noteUtil, args.metrics);
+          new ChangeNotesParser(
+              key.changeId(),
+              key.id(),
+              rw,
+              args.changeNoteJson,
+              args.legacyChangeNoteRead,
+              args.metrics);
       ChangeNotesState result = parser.parseAll();
       // This assignment only happens if call() was actually called, which only
       // happens when Cache#get(K, Callable<V>) incurs a cache miss.
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 2eb30ff..5f2593b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -51,6 +51,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
 import com.google.common.collect.Tables;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -100,11 +101,9 @@
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.util.GitDateParser;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class ChangeNotesParser {
-  private static final Logger log = LoggerFactory.getLogger(ChangeNotesParser.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   // Sentinel RevId indicating a mutable field on a patch set was parsed, but
   // the parser does not yet know its commit SHA-1.
@@ -124,7 +123,9 @@
   }
 
   // Private final members initialized in the constructor.
-  private final ChangeNoteUtil noteUtil;
+  private final ChangeNoteJson changeNoteJson;
+  private final LegacyChangeNoteRead legacyChangeNoteRead;
+
   private final NoteDbMetrics metrics;
   private final Change.Id id;
   private final ObjectId tip;
@@ -175,12 +176,14 @@
       Change.Id changeId,
       ObjectId tip,
       ChangeNotesRevWalk walk,
-      ChangeNoteUtil noteUtil,
+      ChangeNoteJson changeNoteJson,
+      LegacyChangeNoteRead legacyChangeNoteRead,
       NoteDbMetrics metrics) {
     this.id = changeId;
     this.tip = tip;
     this.walk = walk;
-    this.noteUtil = noteUtil;
+    this.changeNoteJson = changeNoteJson;
+    this.legacyChangeNoteRead = legacyChangeNoteRead;
     this.metrics = metrics;
     approvals = new LinkedHashMap<>();
     bufferedApprovals = new ArrayList<>();
@@ -446,7 +449,7 @@
       return effectiveAccountId;
     }
     PersonIdent ident = RawParseUtils.parsePersonIdent(realUser);
-    return noteUtil.parseIdent(ident, id);
+    return legacyChangeNoteRead.parseIdent(ident, id);
   }
 
   private String parseTopic(ChangeNotesCommit commit) throws ConfigInvalidException {
@@ -581,7 +584,7 @@
         parsedAssignee = Optional.empty();
       } else {
         PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
-        parsedAssignee = Optional.ofNullable(noteUtil.parseIdent(ident, id));
+        parsedAssignee = Optional.ofNullable(legacyChangeNoteRead.parseIdent(ident, id));
       }
       if (assignee == null) {
         assignee = parsedAssignee;
@@ -749,7 +752,8 @@
     ChangeNotesCommit tipCommit = walk.parseCommit(tip);
     revisionNoteMap =
         RevisionNoteMap.parse(
-            noteUtil,
+            changeNoteJson,
+            legacyChangeNoteRead,
             id,
             reader,
             NoteMap.read(reader, tipCommit),
@@ -807,7 +811,7 @@
       labelVoteStr = line.substring(0, s);
       PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
       checkFooter(ident != null, FOOTER_LABEL, line);
-      effectiveAccountId = noteUtil.parseIdent(ident, id);
+      effectiveAccountId = legacyChangeNoteRead.parseIdent(ident, id);
     } else {
       labelVoteStr = line;
       effectiveAccountId = committerId;
@@ -849,7 +853,7 @@
       label = line.substring(1, s);
       PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
       checkFooter(ident != null, FOOTER_LABEL, line);
-      effectiveAccountId = noteUtil.parseIdent(ident, id);
+      effectiveAccountId = legacyChangeNoteRead.parseIdent(ident, id);
     } else {
       label = line.substring(1);
       effectiveAccountId = committerId;
@@ -913,7 +917,7 @@
           label.label = line.substring(c + 2, c2);
           PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(c2 + 2));
           checkFooter(ident != null, FOOTER_SUBMITTED_WITH, line);
-          label.appliedBy = noteUtil.parseIdent(ident, id);
+          label.appliedBy = legacyChangeNoteRead.parseIdent(ident, id);
         } else {
           label.label = line.substring(c + 2);
         }
@@ -929,7 +933,7 @@
     if (a.getName().equals(c.getName()) && a.getEmailAddress().equals(c.getEmailAddress())) {
       return null;
     }
-    return noteUtil.parseIdent(commit.getAuthorIdent(), id);
+    return legacyChangeNoteRead.parseIdent(commit.getAuthorIdent(), id);
   }
 
   private void parseReviewer(Timestamp ts, ReviewerStateInternal state, String line)
@@ -938,7 +942,7 @@
     if (ident == null) {
       throw invalidFooter(state.getFooterKey(), line);
     }
-    Account.Id accountId = noteUtil.parseIdent(ident, id);
+    Account.Id accountId = legacyChangeNoteRead.parseIdent(ident, id);
     reviewerUpdates.add(ReviewerStatusUpdate.create(ts, ownerId, accountId, state));
     if (!reviewers.containsRow(accountId)) {
       reviewers.put(accountId, state, ts);
@@ -1087,7 +1091,8 @@
             approvals.values(), PatchSetApproval::getPatchSetId, missing);
 
     if (!missing.isEmpty()) {
-      log.warn("ignoring {} additional entities due to missing patch sets: {}", pruned, missing);
+      logger.atWarning().log(
+          "ignoring %s additional entities due to missing patch sets: %s", pruned, missing);
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 1b09494..3eb06b2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -54,6 +54,7 @@
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
@@ -63,13 +64,11 @@
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gson.Gson;
-import com.google.protobuf.ByteString;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -447,9 +446,7 @@
       checkArgument(object.columns() != null, "ChangeColumns is required in: %s", object);
       ChangeNotesStateProto.Builder b = ChangeNotesStateProto.newBuilder();
 
-      byte[] idBuf = new byte[Constants.OBJECT_ID_LENGTH];
-      object.metaId().copyRawTo(idBuf, 0);
-      b.setMetaId(ByteString.copyFrom(idBuf))
+      b.setMetaId(ObjectIdConverter.create().toByteString(object.metaId()))
           .setChangeId(object.changeId().get())
           .setColumns(toChangeColumnsProto(object.columns()));
 
@@ -555,18 +552,13 @@
 
     @Override
     public ChangeNotesState deserialize(byte[] in) {
-      ChangeNotesStateProto proto;
-      try {
-        proto = ChangeNotesStateProto.parseFrom(in);
-      } catch (IOException e) {
-        throw new IllegalArgumentException(
-            "Failed to deserialize " + ChangeNotesState.class.getName());
-      }
+      ChangeNotesStateProto proto =
+          ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), in);
       Change.Id changeId = new Change.Id(proto.getChangeId());
 
       ChangeNotesState.Builder b =
           builder()
-              .metaId(ObjectId.fromRaw(proto.getMetaId().toByteArray()))
+              .metaId(ObjectIdConverter.create().fromByteString(proto.getMetaId()))
               .changeId(changeId)
               .columns(toChangeColumns(changeId, proto.getColumns()))
               .pastAssignees(
diff --git a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
index 35e4a12..894e979 100644
--- a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
@@ -37,19 +37,22 @@
   // See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
   private static final byte[] END_SIGNATURE = "-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
 
-  private final ChangeNoteUtil noteUtil;
+  private final ChangeNoteJson noteJson;
+  private final LegacyChangeNoteRead legacyChangeNoteRead;
   private final Change.Id changeId;
   private final PatchLineComment.Status status;
   private String pushCert;
 
   ChangeRevisionNote(
-      ChangeNoteUtil noteUtil,
+      ChangeNoteJson noteJson,
+      LegacyChangeNoteRead legacyChangeNoteRead,
       Change.Id changeId,
       ObjectReader reader,
       ObjectId noteId,
       PatchLineComment.Status status) {
     super(reader, noteId);
-    this.noteUtil = noteUtil;
+    this.legacyChangeNoteRead = legacyChangeNoteRead;
+    this.noteJson = noteJson;
     this.changeId = changeId;
     this.status = status;
   }
@@ -65,7 +68,7 @@
     p.value = offset;
 
     if (isJson(raw, p.value)) {
-      RevisionNoteData data = parseJson(noteUtil, raw, p.value);
+      RevisionNoteData data = parseJson(noteJson, raw, p.value);
       if (status == PatchLineComment.Status.PUBLISHED) {
         pushCert = data.pushCert;
       } else {
@@ -80,7 +83,7 @@
     } else {
       pushCert = null;
     }
-    List<Comment> comments = noteUtil.parseNote(raw, p, changeId);
+    List<Comment> comments = legacyChangeNoteRead.parseNote(raw, p, changeId);
     comments.forEach(c -> c.legacyFormat = true);
     return comments;
   }
@@ -89,7 +92,7 @@
     return raw[offset] == '{' || raw[offset] == '[';
   }
 
-  private RevisionNoteData parseJson(ChangeNoteUtil noteUtil, byte[] raw, int offset)
+  private RevisionNoteData parseJson(ChangeNoteJson noteUtil, byte[] raw, int offset)
       throws IOException {
     try (InputStream is = new ByteArrayInputStream(raw, offset, raw.length - offset);
         Reader r = new InputStreamReader(is, UTF_8)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index ccc5859..445f7a0 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -40,7 +40,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.sanitizeFooter;
+import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
 import static java.util.Comparator.comparing;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -527,7 +527,13 @@
 
     for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
       ObjectId data =
-          inserter.insert(OBJ_BLOB, e.getValue().build(noteUtil, noteUtil.getWriteJson()));
+          inserter.insert(
+              OBJ_BLOB,
+              e.getValue()
+                  .build(
+                      noteUtil.getChangeNoteJson(),
+                      noteUtil.getLegacyChangeNoteWrite(),
+                      noteUtil.getChangeNoteJson().getWriteJson()));
       rnm.noteMap.set(ObjectId.fromString(e.getKey().get()), data);
     }
 
@@ -555,7 +561,12 @@
     // Even though reading from changes might not be enabled, we need to
     // parse any existing revision notes so we can merge them.
     return RevisionNoteMap.parse(
-        noteUtil, getId(), rw.getObjectReader(), noteMap, PatchLineComment.Status.PUBLISHED);
+        noteUtil.getChangeNoteJson(),
+        noteUtil.getLegacyChangeNoteRead(),
+        getId(),
+        rw.getObjectReader(),
+        noteMap,
+        PatchLineComment.Status.PUBLISHED);
   }
 
   private void checkComments(
@@ -738,7 +749,7 @@
     }
 
     if (readOnlyUntil != null) {
-      addFooter(msg, FOOTER_READ_ONLY_UNTIL, ChangeNoteUtil.formatTime(serverIdent, readOnlyUntil));
+      addFooter(msg, FOOTER_READ_ONLY_UNTIL, NoteDbUtil.formatTime(serverIdent, readOnlyUntil));
     }
 
     if (isPrivate != null) {
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index b3907eb..9a8c130 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -133,9 +133,14 @@
    */
   @VisibleForTesting
   public static Map<String, Comment> getPublishedComments(
-      ChangeNoteUtil noteUtil, Change.Id changeId, ObjectReader reader, NoteMap noteMap)
+      ChangeNoteJson changeNoteJson,
+      LegacyChangeNoteRead legacyChangeNoteRead,
+      Change.Id changeId,
+      ObjectReader reader,
+      NoteMap noteMap)
       throws IOException, ConfigInvalidException {
-    return RevisionNoteMap.parse(noteUtil, changeId, reader, noteMap, PUBLISHED)
+    return RevisionNoteMap.parse(
+            changeNoteJson, legacyChangeNoteRead, changeId, reader, noteMap, PUBLISHED)
         .revisionNotes
         .values()
         .stream()
@@ -143,6 +148,16 @@
         .collect(toMap(c -> c.key.uuid, Function.identity()));
   }
 
+  public static Map<String, Comment> getPublishedComments(
+      ChangeNoteUtil noteUtil, Change.Id changeId, ObjectReader reader, NoteMap noteMap)
+      throws IOException, ConfigInvalidException {
+    return getPublishedComments(
+        noteUtil.getChangeNoteJson(),
+        noteUtil.getLegacyChangeNoteRead(),
+        changeId,
+        reader,
+        noteMap);
+  }
   /**
    * Gets the comments put in by the current commit. The message of the target comment will be
    * replaced by the new message.
@@ -205,7 +220,12 @@
       throws IOException, ConfigInvalidException {
     RevisionNoteMap<ChangeRevisionNote> revNotesMap =
         RevisionNoteMap.parse(
-            noteUtil, changeId, reader, NoteMap.read(reader, parentCommit), PUBLISHED);
+            noteUtil.getChangeNoteJson(),
+            noteUtil.getLegacyChangeNoteRead(),
+            changeId,
+            reader,
+            NoteMap.read(reader, parentCommit),
+            PUBLISHED);
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNotesMap);
 
     for (Comment c : putInComments) {
@@ -219,7 +239,13 @@
     Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
     for (Map.Entry<RevId, RevisionNoteBuilder> entry : builders.entrySet()) {
       ObjectId objectId = ObjectId.fromString(entry.getKey().get());
-      byte[] data = entry.getValue().build(noteUtil, noteUtil.getWriteJson());
+      byte[] data =
+          entry
+              .getValue()
+              .build(
+                  noteUtil.getChangeNoteJson(),
+                  noteUtil.getLegacyChangeNoteWrite(),
+                  noteUtil.getChangeNoteJson().getWriteJson());
       if (data.length == 0) {
         revNotesMap.noteMap.remove(objectId);
       } else {
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 008f31f..79da7e1 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Account;
@@ -50,12 +51,10 @@
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** View of the draft comments for a single {@link Change} based on the log of its drafts branch. */
 public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
-  private static final Logger log = LoggerFactory.getLogger(DraftCommentNotes.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     DraftCommentNotes create(Change change, Account.Id accountId);
@@ -155,7 +154,8 @@
     ObjectReader reader = handle.walk().getObjectReader();
     revisionNoteMap =
         RevisionNoteMap.parse(
-            args.noteUtil,
+            args.changeNoteJson,
+            args.legacyChangeNoteRead,
             getChangeId(),
             reader,
             NoteMap.read(reader, tipCommit),
@@ -224,7 +224,8 @@
           repo.scanForRepoChanges();
         } catch (OrmException | IOException e) {
           // See ChangeNotes#rebuildAndOpen.
-          log.debug("Rebuilding change {} via drafts failed: {}", getChangeId(), e.getMessage());
+          logger.atFine().log(
+              "Rebuilding change %s via drafts failed: %s", getChangeId(), e.getMessage());
           args.metrics.autoRebuildFailureCount.increment(CHANGES);
           checkNotNull(r.staged());
           return LoadHandle.create(
@@ -237,8 +238,8 @@
     } catch (OrmException e) {
       throw new IOException(e);
     } finally {
-      log.debug(
-          "Rebuilt change {} in {} in {} ms via drafts",
+      logger.atFine().log(
+          "Rebuilt change %s in %s in %s ms via drafts",
           getChangeId(),
           change != null ? "project " + change.getProject() : "unknown project",
           TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS));
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
new file mode 100644
index 0000000..819c8ac
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
@@ -0,0 +1,402 @@
+// 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.notedb;
+
+import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Comment.Key;
+import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.util.GitDateParser;
+import org.eclipse.jgit.util.MutableInteger;
+import org.eclipse.jgit.util.QuotedString;
+import org.eclipse.jgit.util.RawParseUtils;
+
+public class LegacyChangeNoteRead {
+  private final String serverId;
+
+  @Inject
+  public LegacyChangeNoteRead(@GerritServerId String serverId) {
+    this.serverId = serverId;
+  }
+
+  public Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
+      throws ConfigInvalidException {
+    return NoteDbUtil.parseIdent(ident, serverId)
+        .orElseThrow(
+            () ->
+                parseException(
+                    changeId,
+                    "invalid identity, expected <id>@%s: %s",
+                    serverId,
+                    ident.getEmailAddress()));
+  }
+
+  private static boolean match(byte[] note, MutableInteger p, byte[] expected) {
+    int m = RawParseUtils.match(note, p.value, expected);
+    return m == p.value + expected.length;
+  }
+
+  public List<Comment> parseNote(byte[] note, MutableInteger p, Change.Id changeId)
+      throws ConfigInvalidException {
+    if (p.value >= note.length) {
+      return ImmutableList.of();
+    }
+    Set<Key> seen = new HashSet<>();
+    List<Comment> result = new ArrayList<>();
+    int sizeOfNote = note.length;
+    byte[] psb = ChangeNoteUtil.PATCH_SET.getBytes(UTF_8);
+    byte[] bpsb = ChangeNoteUtil.BASE_PATCH_SET.getBytes(UTF_8);
+    byte[] bpn = ChangeNoteUtil.PARENT_NUMBER.getBytes(UTF_8);
+
+    RevId revId = new RevId(parseStringField(note, p, changeId, ChangeNoteUtil.REVISION));
+    String fileName = null;
+    PatchSet.Id psId = null;
+    boolean isForBase = false;
+    Integer parentNumber = null;
+
+    while (p.value < sizeOfNote) {
+      boolean matchPs = match(note, p, psb);
+      boolean matchBase = match(note, p, bpsb);
+      if (matchPs) {
+        fileName = null;
+        psId = parsePsId(note, p, changeId, ChangeNoteUtil.PATCH_SET);
+        isForBase = false;
+      } else if (matchBase) {
+        fileName = null;
+        psId = parsePsId(note, p, changeId, ChangeNoteUtil.BASE_PATCH_SET);
+        isForBase = true;
+        if (match(note, p, bpn)) {
+          parentNumber = parseParentNumber(note, p, changeId);
+        }
+      } else if (psId == null) {
+        throw parseException(
+            changeId,
+            "missing %s or %s header",
+            ChangeNoteUtil.PATCH_SET,
+            ChangeNoteUtil.BASE_PATCH_SET);
+      }
+
+      Comment c = parseComment(note, p, fileName, psId, revId, isForBase, parentNumber);
+      fileName = c.key.filename;
+      if (!seen.add(c.key)) {
+        throw parseException(changeId, "multiple comments for %s in note", c.key);
+      }
+      result.add(c);
+    }
+    return result;
+  }
+
+  private Comment parseComment(
+      byte[] note,
+      MutableInteger curr,
+      String currentFileName,
+      PatchSet.Id psId,
+      RevId revId,
+      boolean isForBase,
+      Integer parentNumber)
+      throws ConfigInvalidException {
+    Change.Id changeId = psId.getParentKey();
+
+    // Check if there is a new file.
+    boolean newFile =
+        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.FILE.getBytes(UTF_8))) != -1;
+    if (newFile) {
+      // If so, parse the new file name.
+      currentFileName = parseFilename(note, curr, changeId);
+    } else if (currentFileName == null) {
+      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.FILE);
+    }
+
+    CommentRange range = parseCommentRange(note, curr);
+    if (range == null) {
+      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.COMMENT_RANGE);
+    }
+
+    Timestamp commentTime = parseTimestamp(note, curr, changeId);
+    Account.Id aId = parseAuthor(note, curr, changeId, ChangeNoteUtil.AUTHOR);
+    boolean hasRealAuthor =
+        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.REAL_AUTHOR.getBytes(UTF_8))) != -1;
+    Account.Id raId = null;
+    if (hasRealAuthor) {
+      raId = parseAuthor(note, curr, changeId, ChangeNoteUtil.REAL_AUTHOR);
+    }
+
+    boolean hasParent =
+        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.PARENT.getBytes(UTF_8))) != -1;
+    String parentUUID = null;
+    boolean unresolved = false;
+    if (hasParent) {
+      parentUUID = parseStringField(note, curr, changeId, ChangeNoteUtil.PARENT);
+    }
+    boolean hasUnresolved =
+        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.UNRESOLVED.getBytes(UTF_8))) != -1;
+    if (hasUnresolved) {
+      unresolved = parseBooleanField(note, curr, changeId, ChangeNoteUtil.UNRESOLVED);
+    }
+
+    String uuid = parseStringField(note, curr, changeId, ChangeNoteUtil.UUID);
+
+    boolean hasTag =
+        (RawParseUtils.match(note, curr.value, ChangeNoteUtil.TAG.getBytes(UTF_8))) != -1;
+    String tag = null;
+    if (hasTag) {
+      tag = parseStringField(note, curr, changeId, ChangeNoteUtil.TAG);
+    }
+
+    int commentLength = parseCommentLength(note, curr, changeId);
+
+    String message = RawParseUtils.decode(UTF_8, note, curr.value, curr.value + commentLength);
+    checkResult(message, "message contents", changeId);
+
+    Comment c =
+        new Comment(
+            new Comment.Key(uuid, currentFileName, psId.get()),
+            aId,
+            commentTime,
+            isForBase ? (short) (parentNumber == null ? 0 : -parentNumber) : (short) 1,
+            message,
+            serverId,
+            unresolved);
+    c.lineNbr = range.getEndLine();
+    c.parentUuid = parentUUID;
+    c.tag = tag;
+    c.setRevId(revId);
+    if (raId != null) {
+      c.setRealAuthor(raId);
+    }
+
+    if (range.getStartCharacter() != -1) {
+      c.setRange(range);
+    }
+
+    curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
+    curr.value = RawParseUtils.nextLF(note, curr.value);
+    return c;
+  }
+
+  private static String parseStringField(
+      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
+      throws ConfigInvalidException {
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    checkHeaderLineFormat(note, curr, fieldName, changeId);
+    int startOfField = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
+    curr.value = endOfLine;
+    return RawParseUtils.decode(UTF_8, note, startOfField, endOfLine - 1);
+  }
+
+  /**
+   * @return a comment range. If the comment range line in the note only has one number, we return a
+   *     CommentRange with that one number as the end line and the other fields as -1. If the
+   *     comment range line in the note contains a whole comment range, then we return a
+   *     CommentRange with all fields set. If the line is not correctly formatted, return null.
+   */
+  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
+    CommentRange range = new CommentRange(-1, -1, -1, -1);
+
+    int last = ptr.value;
+    int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (ptr.value == last) {
+      return null;
+    } else if (note[ptr.value] == '\n') {
+      range.setEndLine(startLine);
+      ptr.value += 1;
+      return range;
+    } else if (note[ptr.value] == ':') {
+      range.setStartLine(startLine);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+
+    last = ptr.value;
+    int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (ptr.value == last) {
+      return null;
+    } else if (note[ptr.value] == '-') {
+      range.setStartCharacter(startChar);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+
+    last = ptr.value;
+    int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (ptr.value == last) {
+      return null;
+    } else if (note[ptr.value] == ':') {
+      range.setEndLine(endLine);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+
+    last = ptr.value;
+    int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
+    if (ptr.value == last) {
+      return null;
+    } else if (note[ptr.value] == '\n') {
+      range.setEndCharacter(endChar);
+      ptr.value += 1;
+    } else {
+      return null;
+    }
+    return range;
+  }
+
+  private static PatchSet.Id parsePsId(
+      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
+      throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, fieldName, changeId);
+    int startOfPsId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+    MutableInteger i = new MutableInteger();
+    int patchSetId = RawParseUtils.parseBase10(note, startOfPsId, i);
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    if (i.value != endOfLine - 1) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+    checkResult(patchSetId, "patchset id", changeId);
+    curr.value = endOfLine;
+    return new PatchSet.Id(changeId, patchSetId);
+  }
+
+  private static Integer parseParentNumber(byte[] note, MutableInteger curr, Change.Id changeId)
+      throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, ChangeNoteUtil.PARENT_NUMBER, changeId);
+
+    int start = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+    MutableInteger i = new MutableInteger();
+    int parentNumber = RawParseUtils.parseBase10(note, start, i);
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    if (i.value != endOfLine - 1) {
+      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.PARENT_NUMBER);
+    }
+    checkResult(parentNumber, "parent number", changeId);
+    curr.value = endOfLine;
+    return Integer.valueOf(parentNumber);
+  }
+
+  private static String parseFilename(byte[] note, MutableInteger curr, Change.Id changeId)
+      throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, ChangeNoteUtil.FILE, changeId);
+    int startOfFileName = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    curr.value = endOfLine;
+    curr.value = RawParseUtils.nextLF(note, curr.value);
+    return QuotedString.GIT_PATH.dequote(
+        RawParseUtils.decode(UTF_8, note, startOfFileName, endOfLine - 1));
+  }
+
+  private static Timestamp parseTimestamp(byte[] note, MutableInteger curr, Change.Id changeId)
+      throws ConfigInvalidException {
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    Timestamp commentTime;
+    String dateString = RawParseUtils.decode(UTF_8, note, curr.value, endOfLine - 1);
+    try {
+      commentTime = new Timestamp(GitDateParser.parse(dateString, null, Locale.US).getTime());
+    } catch (ParseException e) {
+      throw new ConfigInvalidException("could not parse comment timestamp", e);
+    }
+    curr.value = endOfLine;
+    return checkResult(commentTime, "comment timestamp", changeId);
+  }
+
+  private Account.Id parseAuthor(
+      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
+      throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, fieldName, changeId);
+    int startOfAccountId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
+    PersonIdent ident = RawParseUtils.parsePersonIdent(note, startOfAccountId);
+    Account.Id aId = parseIdent(ident, changeId);
+    curr.value = RawParseUtils.nextLF(note, curr.value);
+    return checkResult(aId, fieldName, changeId);
+  }
+
+  private static int parseCommentLength(byte[] note, MutableInteger curr, Change.Id changeId)
+      throws ConfigInvalidException {
+    checkHeaderLineFormat(note, curr, ChangeNoteUtil.LENGTH, changeId);
+    int startOfLength = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
+    MutableInteger i = new MutableInteger();
+    i.value = startOfLength;
+    int commentLength = RawParseUtils.parseBase10(note, startOfLength, i);
+    if (i.value == startOfLength) {
+      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.LENGTH);
+    }
+    int endOfLine = RawParseUtils.nextLF(note, curr.value);
+    if (i.value != endOfLine - 1) {
+      throw parseException(changeId, "could not parse %s", ChangeNoteUtil.LENGTH);
+    }
+    curr.value = endOfLine;
+    return checkResult(commentLength, "comment length", changeId);
+  }
+
+  private boolean parseBooleanField(
+      byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
+      throws ConfigInvalidException {
+    String str = parseStringField(note, curr, changeId, fieldName);
+    if ("true".equalsIgnoreCase(str)) {
+      return true;
+    } else if ("false".equalsIgnoreCase(str)) {
+      return false;
+    }
+    throw parseException(changeId, "invalid boolean for %s: %s", fieldName, str);
+  }
+
+  private static <T> T checkResult(T o, String fieldName, Change.Id changeId)
+      throws ConfigInvalidException {
+    if (o == null) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+    return o;
+  }
+
+  private static int checkResult(int i, String fieldName, Change.Id changeId)
+      throws ConfigInvalidException {
+    if (i <= 0) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+    return i;
+  }
+
+  private static void checkHeaderLineFormat(
+      byte[] note, MutableInteger curr, String fieldName, Change.Id changeId)
+      throws ConfigInvalidException {
+    boolean correct = RawParseUtils.match(note, curr.value, fieldName.getBytes(UTF_8)) != -1;
+    int p = curr.value + fieldName.length();
+    correct &= (p < note.length && note[p] == ':');
+    p++;
+    correct &= (p < note.length && note[p] == ' ');
+    if (!correct) {
+      throw parseException(changeId, "could not parse %s", fieldName);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
new file mode 100644
index 0000000..1cf0c7c
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
@@ -0,0 +1,206 @@
+// 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.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.inject.Inject;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.util.QuotedString;
+
+public class LegacyChangeNoteWrite {
+
+  private final AccountCache accountCache;
+  private final PersonIdent serverIdent;
+  private final String serverId;
+
+  @Inject
+  public LegacyChangeNoteWrite(
+      AccountCache accountCache,
+      @GerritPersonIdent PersonIdent serverIdent,
+      @GerritServerId String serverId) {
+    this.accountCache = accountCache;
+    this.serverIdent = serverIdent;
+    this.serverId = serverId;
+  }
+
+  public PersonIdent newIdent(Account.Id authorId, Date when, PersonIdent serverIdent) {
+    Optional<Account> author = accountCache.get(authorId).map(AccountState::getAccount);
+    return new PersonIdent(
+        author.map(Account::getName).orElseGet(() -> Account.getName(authorId)),
+        authorId.get() + "@" + serverId,
+        when,
+        serverIdent.getTimeZone());
+  }
+
+  @VisibleForTesting
+  public PersonIdent newIdent(Account author, Date when, PersonIdent serverIdent) {
+    return new PersonIdent(
+        author.getName(), author.getId().get() + "@" + serverId, when, serverIdent.getTimeZone());
+  }
+
+  public String getServerId() {
+    return serverId;
+  }
+
+  private void appendHeaderField(PrintWriter writer, String field, String value) {
+    writer.print(field);
+    writer.print(": ");
+    writer.print(value);
+    writer.print('\n');
+  }
+
+  /**
+   * Build a note that contains the metadata for and the contents of all of the comments in the
+   * given comments.
+   *
+   * @param comments Comments to be written to the output stream, keyed by patch set ID; multiple
+   *     patch sets are allowed since base revisions may be shared across patch sets. All of the
+   *     comments must share the same RevId, and all the comments for a given patch set must have
+   *     the same side.
+   * @param out output stream to write to.
+   */
+  void buildNote(ListMultimap<Integer, Comment> comments, OutputStream out) {
+    if (comments.isEmpty()) {
+      return;
+    }
+
+    List<Integer> psIds = new ArrayList<>(comments.keySet());
+    Collections.sort(psIds);
+
+    OutputStreamWriter streamWriter = new OutputStreamWriter(out, UTF_8);
+    try (PrintWriter writer = new PrintWriter(streamWriter)) {
+      String revId = comments.values().iterator().next().revId;
+      appendHeaderField(writer, ChangeNoteUtil.REVISION, revId);
+
+      for (int psId : psIds) {
+        List<Comment> psComments = COMMENT_ORDER.sortedCopy(comments.get(psId));
+        Comment first = psComments.get(0);
+
+        short side = first.side;
+        appendHeaderField(
+            writer,
+            side <= 0 ? ChangeNoteUtil.BASE_PATCH_SET : ChangeNoteUtil.PATCH_SET,
+            Integer.toString(psId));
+        if (side < 0) {
+          appendHeaderField(writer, ChangeNoteUtil.PARENT_NUMBER, Integer.toString(-side));
+        }
+
+        String currentFilename = null;
+
+        for (Comment c : psComments) {
+          checkArgument(
+              revId.equals(c.revId),
+              "All comments being added must have all the same RevId. The "
+                  + "comment below does not have the same RevId as the others "
+                  + "(%s).\n%s",
+              revId,
+              c);
+          checkArgument(
+              side == c.side,
+              "All comments being added must all have the same side. The "
+                  + "comment below does not have the same side as the others "
+                  + "(%s).\n%s",
+              side,
+              c);
+          String commentFilename = QuotedString.GIT_PATH.quote(c.key.filename);
+
+          if (!commentFilename.equals(currentFilename)) {
+            currentFilename = commentFilename;
+            writer.print("File: ");
+            writer.print(commentFilename);
+            writer.print("\n\n");
+          }
+
+          appendOneComment(writer, c);
+        }
+      }
+    }
+  }
+
+  private void appendOneComment(PrintWriter writer, Comment c) {
+    // The CommentRange field for a comment is allowed to be null. If it is
+    // null, then in the first line, we simply use the line number field for a
+    // comment instead. If it isn't null, we write the comment range itself.
+    Comment.Range range = c.range;
+    if (range != null) {
+      writer.print(range.startLine);
+      writer.print(':');
+      writer.print(range.startChar);
+      writer.print('-');
+      writer.print(range.endLine);
+      writer.print(':');
+      writer.print(range.endChar);
+    } else {
+      writer.print(c.lineNbr);
+    }
+    writer.print("\n");
+
+    writer.print(NoteDbUtil.formatTime(serverIdent, c.writtenOn));
+    writer.print("\n");
+
+    appendIdent(writer, ChangeNoteUtil.AUTHOR, c.author.getId(), c.writtenOn);
+    if (!c.getRealAuthor().equals(c.author)) {
+      appendIdent(writer, ChangeNoteUtil.REAL_AUTHOR, c.getRealAuthor().getId(), c.writtenOn);
+    }
+
+    String parent = c.parentUuid;
+    if (parent != null) {
+      appendHeaderField(writer, ChangeNoteUtil.PARENT, parent);
+    }
+
+    appendHeaderField(writer, ChangeNoteUtil.UNRESOLVED, Boolean.toString(c.unresolved));
+    appendHeaderField(writer, ChangeNoteUtil.UUID, c.key.uuid);
+
+    if (c.tag != null) {
+      appendHeaderField(writer, ChangeNoteUtil.TAG, c.tag);
+    }
+
+    byte[] messageBytes = c.message.getBytes(UTF_8);
+    appendHeaderField(writer, ChangeNoteUtil.LENGTH, Integer.toString(messageBytes.length));
+
+    writer.print(c.message);
+    writer.print("\n\n");
+  }
+
+  private void appendIdent(PrintWriter writer, String header, Account.Id id, Timestamp ts) {
+    PersonIdent ident = newIdent(id, ts, serverIdent);
+    StringBuilder name = new StringBuilder();
+    PersonIdent.appendSanitized(name, ident.getName());
+    name.append(" <");
+    PersonIdent.appendSanitized(name, ident.getEmailAddress());
+    name.append('>');
+    appendHeaderField(writer, header, name.toString());
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 59c4c62..21fada8 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -14,12 +14,21 @@
 
 package com.google.gerrit.server.notedb;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.reviewdb.client.Account;
+import java.sql.Timestamp;
 import java.util.Optional;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.GitDateFormatter.Format;
 
 public class NoteDbUtil {
+
+  /**
+   * Returns an AccountId for the given email address. Returns empty if the address isn't on this
+   * server.
+   */
   public static Optional<Account.Id> parseIdent(PersonIdent ident, String serverId) {
     String email = ident.getEmailAddress();
     int at = email.indexOf('@');
@@ -36,4 +45,24 @@
   }
 
   private NoteDbUtil() {}
+
+  public static String formatTime(PersonIdent ident, Timestamp t) {
+    GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
+    // TODO(dborowitz): Use a ThreadLocal or use Joda.
+    PersonIdent newIdent = new PersonIdent(ident, t);
+    return dateFormatter.formatDate(newIdent);
+  }
+
+  private static final CharMatcher INVALID_FOOTER_CHARS = CharMatcher.anyOf("\r\n\0");
+
+  static String sanitizeFooter(String value) {
+    // Remove characters that would confuse JGit's footer parser if they were
+    // included in footer values, for example by splitting the footer block into
+    // multiple paragraphs.
+    //
+    // One painful example: RevCommit#getShorMessage() might return a message
+    // containing "\r\r", which RevCommit#getFooterLines() will treat as an
+    // empty paragraph for the purposes of footer parsing.
+    return INVALID_FOOTER_CHARS.trimAndCollapseFrom(value, ' ');
+  }
 }
diff --git a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
index 69cc2eb..43ed722 100644
--- a/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
+++ b/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
@@ -27,6 +27,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Stopwatch;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -72,13 +73,11 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Helper to migrate the {@link PrimaryStorage} of individual changes. */
 @Singleton
 public class PrimaryStorageMigrator {
-  private static final Logger log = LoggerFactory.getLogger(PrimaryStorageMigrator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /**
    * Exception thrown during migration if the change has no {@code noteDbState} field at the
@@ -277,7 +276,8 @@
     // the primary storage to NoteDb.
 
     setPrimaryStorageNoteDb(id, rebuiltState);
-    log.debug("Migrated change {} to NoteDb primary in {}ms", id, sw.elapsed(MILLISECONDS));
+    logger.atFine().log(
+        "Migrated change %s to NoteDb primary in %sms", id, sw.elapsed(MILLISECONDS));
   }
 
   private Change setReadOnlyInReviewDb(Change.Id id) throws OrmException {
@@ -413,7 +413,8 @@
     rebuilder.rebuildReviewDb(db(), project, id);
     setPrimaryStorageReviewDb(id, newMetaId);
     releaseReadOnlyLeaseInNoteDb(project, id);
-    log.debug("Migrated change {} to ReviewDb primary in {}ms", id, sw.elapsed(MILLISECONDS));
+    logger.atFine().log(
+        "Migrated change %s to ReviewDb primary in %sms", id, sw.elapsed(MILLISECONDS));
   }
 
   private ObjectId setReadOnlyInNoteDb(Project.NameKey project, Change.Id id)
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index b341ea8..8bf286d 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -83,11 +83,17 @@
   }
 
   public byte[] build(ChangeNoteUtil noteUtil, boolean writeJson) throws IOException {
+    return build(noteUtil.getChangeNoteJson(), noteUtil.getLegacyChangeNoteWrite(), writeJson);
+  }
+
+  public byte[] build(
+      ChangeNoteJson changeNoteJson, LegacyChangeNoteWrite legacyChangeNoteWrite, boolean writeJson)
+      throws IOException {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     if (writeJson) {
-      buildNoteJson(noteUtil, out);
+      buildNoteJson(changeNoteJson, out);
     } else {
-      buildNoteLegacy(noteUtil, out);
+      buildNoteLegacy(legacyChangeNoteWrite, out);
     }
     return out.toByteArray();
   }
@@ -122,7 +128,7 @@
     return all;
   }
 
-  private void buildNoteJson(ChangeNoteUtil noteUtil, OutputStream out) throws IOException {
+  private void buildNoteJson(ChangeNoteJson noteUtil, OutputStream out) throws IOException {
     ListMultimap<Integer, Comment> comments = buildCommentMap();
     if (comments.isEmpty() && pushCert == null) {
       return;
@@ -137,7 +143,8 @@
     }
   }
 
-  private void buildNoteLegacy(ChangeNoteUtil noteUtil, OutputStream out) throws IOException {
+  private void buildNoteLegacy(LegacyChangeNoteWrite noteUtil, OutputStream out)
+      throws IOException {
     if (pushCert != null) {
       byte[] certBytes = pushCert.getBytes(UTF_8);
       out.write(certBytes, 0, trimTrailingNewlines(certBytes));
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index aa82d1a..17a061a 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -32,7 +32,8 @@
   final ImmutableMap<RevId, T> revisionNotes;
 
   static RevisionNoteMap<ChangeRevisionNote> parse(
-      ChangeNoteUtil noteUtil,
+      ChangeNoteJson noteJson,
+      LegacyChangeNoteRead legacyChangeNoteRead,
       Change.Id changeId,
       ObjectReader reader,
       NoteMap noteMap,
@@ -41,7 +42,8 @@
     Map<RevId, ChangeRevisionNote> result = new HashMap<>();
     for (Note note : noteMap) {
       ChangeRevisionNote rn =
-          new ChangeRevisionNote(noteUtil, changeId, reader, note.getData(), status);
+          new ChangeRevisionNote(
+              noteJson, legacyChangeNoteRead, changeId, reader, note.getData(), status);
       rn.parse();
       result.put(new RevId(note.name()), rn);
     }
@@ -49,12 +51,12 @@
   }
 
   static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments(
-      ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap)
+      ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
       throws ConfigInvalidException, IOException {
     Map<RevId, RobotCommentsRevisionNote> result = new HashMap<>();
     for (Note note : noteMap) {
       RobotCommentsRevisionNote rn =
-          new RobotCommentsRevisionNote(noteUtil, reader, note.getData());
+          new RobotCommentsRevisionNote(changeNoteJson, reader, note.getData());
       rn.parse();
       result.put(new RevId(note.name()), rn);
     }
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index 99d9615..7eb3a54 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -89,7 +89,8 @@
     RevCommit tipCommit = handle.walk().parseCommit(metaId);
     ObjectReader reader = handle.walk().getObjectReader();
     revisionNoteMap =
-        RevisionNoteMap.parseRobotComments(args.noteUtil, reader, NoteMap.read(reader, tipCommit));
+        RevisionNoteMap.parseRobotComments(
+            args.changeNoteJson, reader, NoteMap.read(reader, tipCommit));
     ListMultimap<RevId, RobotComment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
     for (RobotCommentsRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
       for (RobotComment c : rn.getComments()) {
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index c28125f..3a0d595 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -202,7 +202,8 @@
     }
     // Even though reading from changes might not be enabled, we need to
     // parse any existing revision notes so we can merge them.
-    return RevisionNoteMap.parseRobotComments(noteUtil, rw.getObjectReader(), noteMap);
+    return RevisionNoteMap.parseRobotComments(
+        noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
index aa229ab..6c3cc86 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
@@ -27,9 +27,9 @@
 import org.eclipse.jgit.lib.ObjectReader;
 
 public class RobotCommentsRevisionNote extends RevisionNote<RobotComment> {
-  private final ChangeNoteUtil noteUtil;
+  private final ChangeNoteJson noteUtil;
 
-  RobotCommentsRevisionNote(ChangeNoteUtil noteUtil, ObjectReader reader, ObjectId noteId) {
+  RobotCommentsRevisionNote(ChangeNoteJson noteUtil, ObjectReader reader, ObjectId noteId) {
     super(reader, noteId);
     this.noteUtil = noteUtil;
   }
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 92a878c..3a0bfc1 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -546,7 +546,7 @@
     if (id == null) {
       return new PersonIdent(serverIdent, events.getWhen());
     }
-    return changeNoteUtil.newIdent(id, events.getWhen(), serverIdent);
+    return changeNoteUtil.getLegacyChangeNoteWrite().newIdent(id, events.getWhen(), serverIdent);
   }
 
   private List<HashtagsEvent> getHashtagsEvents(Change change, NoteDbUpdateManager manager)
@@ -564,7 +564,10 @@
     for (RevCommit commit : rw) {
       Account.Id authorId;
       try {
-        authorId = changeNoteUtil.parseIdent(commit.getAuthorIdent(), change.getId());
+        authorId =
+            changeNoteUtil
+                .getLegacyChangeNoteRead()
+                .parseIdent(commit.getAuthorIdent(), change.getId());
       } catch (ConfigInvalidException e) {
         continue; // Corrupt data, no valid hashtags in this commit.
       }
diff --git a/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java b/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
index 1fffab4..8f7b387 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
 
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -25,11 +26,9 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class CommentEvent extends Event {
-  private static final Logger log = LoggerFactory.getLogger(CommentEvent.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public final Comment c;
   private final Change change;
@@ -67,11 +66,9 @@
       try {
         setCommentRevId(c, cache, change, ps);
       } catch (PatchListNotAvailableException e) {
-        log.warn(
-            "Unable to determine parent commit of patch set {} ({}); omitting inline comment {}",
-            ps.getId(),
-            ps.getRevision(),
-            c);
+        logger.atWarning().log(
+            "Unable to determine parent commit of patch set %s (%s); omitting inline comment %s",
+            ps.getId(), ps.getRevision(), c);
         return;
       }
     }
diff --git a/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java b/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
index 3bc3a58..2a2795d 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
 
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -25,11 +26,9 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class DraftCommentEvent extends Event {
-  private static final Logger log = LoggerFactory.getLogger(DraftCommentEvent.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public final Comment c;
   private final Change change;
@@ -65,11 +64,10 @@
       try {
         setCommentRevId(c, cache, change, ps);
       } catch (PatchListNotAvailableException e) {
-        log.warn(
-            "Unable to determine parent commit of patch set {} ({}); omitting draft inline comment",
-            ps.getId(),
-            ps.getRevision(),
-            c);
+        logger.atWarning().log(
+            "Unable to determine parent commit of patch set %s (%s);"
+                + " omitting draft inline comment %s",
+            ps.getId(), ps.getRevision(), c);
         return;
       }
     }
diff --git a/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
index 6480e67..6544b23 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.server.config.AllUsersName;
@@ -32,12 +33,10 @@
 import java.io.PrintWriter;
 import java.util.function.Consumer;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class GcAllUsers {
-  private static final Logger log = LoggerFactory.getLogger(GcAllUsers.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AllUsersName allUsers;
   private final GarbageCollection.Factory gcFactory;
@@ -55,7 +54,7 @@
 
   public void runWithLogger() {
     // Print log messages using logger, and skip progress.
-    run(log::info, null);
+    run(s -> logger.atInfo().log(s), null);
   }
 
   public void run(PrintWriter writer) {
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index 59fdde7..e064a8c 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -38,6 +38,7 @@
 import com.google.common.collect.Ordering;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -109,12 +110,10 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.io.NullOutputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** One stop shop for migrating a site's change storage from ReviewDb to NoteDb. */
 public class NoteDbMigrator implements AutoCloseable {
-  private static final Logger log = LoggerFactory.getLogger(NoteDbMigrator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String AUTO_MIGRATE = "autoMigrate";
   private static final String TRIAL = "trial";
@@ -595,7 +594,7 @@
     prev = saveState(prev, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY);
 
     Stopwatch sw = Stopwatch.createStarted();
-    log.info("Setting primary storage to NoteDb");
+    logger.atInfo().log("Setting primary storage to NoteDb");
     List<Change.Id> allChanges;
     try (ReviewDb db = unwrapDb(schemaFactory.open())) {
       allChanges = Streams.stream(db.changes().all()).map(Change::getId).collect(toList());
@@ -615,18 +614,18 @@
                               } catch (NoNoteDbStateException e) {
                                 if (canSkipPrimaryStorageMigration(
                                     ctx.getReviewDbProvider().get(), id)) {
-                                  log.warn(
-                                      "Change {} previously failed to rebuild;"
+                                  logger.atWarning().withCause(e).log(
+                                      "Change %s previously failed to rebuild;"
                                           + " skipping primary storage migration",
-                                      id,
-                                      e);
+                                      id);
                                 } else {
                                   throw e;
                                 }
                               }
                               return true;
                             } catch (Exception e) {
-                              log.error("Error migrating primary storage for " + id, e);
+                              logger.atSevere().withCause(e).log(
+                                  "Error migrating primary storage for %s", id);
                               return false;
                             }
                           }))
@@ -634,10 +633,9 @@
 
       boolean ok = futuresToBoolean(futures, "Error migrating primary storage");
       double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-      log.info(
-          String.format(
-              "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
-              allChanges.size(), t, allChanges.size() / t));
+      logger.atInfo().log(
+          "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n",
+          allChanges.size(), t, allChanges.size() / t);
       if (!ok) {
         throw new MigrationException("Migrating primary storage for some changes failed, see log");
       }
@@ -673,7 +671,8 @@
     try {
       return Iterables.isEmpty(unwrapDb(db).patchSets().byChange(id));
     } catch (Exception e) {
-      log.error("Error checking if change " + id + " can be skipped, assuming no", e);
+      logger.atSevere().withCause(e).log(
+          "Error checking if change %s can be skipped, assuming no", id);
       return false;
     }
   }
@@ -688,7 +687,8 @@
       noteDbConfig.load();
       return NotesMigrationState.forConfig(noteDbConfig);
     } catch (ConfigInvalidException | IllegalArgumentException e) {
-      log.warn("error reading NoteDb migration options from " + noteDbConfig.getFile(), e);
+      logger.atWarning().withCause(e).log(
+          "error reading NoteDb migration options from %s", noteDbConfig.getFile());
       return Optional.empty();
     }
   }
@@ -728,7 +728,7 @@
 
       // Only set in-memory state once it's been persisted to storage.
       globalNotesMigration.setFrom(newState);
-      log.info("Migration state: {} => {}", expectedOldState, newState);
+      logger.atInfo().log("Migration state: %s => %s", expectedOldState, newState);
 
       return newState;
     }
@@ -759,7 +759,7 @@
       throw new MigrationException("Cannot rebuild without noteDb.changes.write=true");
     }
     Stopwatch sw = Stopwatch.createStarted();
-    log.info("Rebuilding changes in NoteDb");
+    logger.atInfo().log("Rebuilding changes in NoteDb");
 
     ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
     List<ListenableFuture<Boolean>> futures = new ArrayList<>();
@@ -773,7 +773,7 @@
                   try {
                     return rebuildProject(contextHelper.getReviewDb(), changesByProject, project);
                   } catch (Exception e) {
-                    log.error("Error rebuilding project " + project, e);
+                    logger.atSevere().withCause(e).log("Error rebuilding project %s", project);
                     return false;
                   }
                 });
@@ -782,10 +782,9 @@
 
       boolean ok = futuresToBoolean(futures, "Error rebuilding projects");
       double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-      log.info(
-          String.format(
-              "Rebuilt %d changes in %.01fs (%.01f/s)\n",
-              changesByProject.size(), t, changesByProject.size() / t));
+      logger.atInfo().log(
+          "Rebuilt %d changes in %.01fs (%.01f/s)\n",
+          changesByProject.size(), t, changesByProject.size() / t);
       if (!ok) {
         throw new MigrationException("Rebuilding some changes failed, see log");
       }
@@ -893,14 +892,14 @@
 
             toSave++;
           } catch (NoPatchSetsException e) {
-            log.warn(e.getMessage());
+            logger.atWarning().log(e.getMessage());
           } catch (ConflictingUpdateException ex) {
-            log.warn(
-                "Rebuilding detected a conflicting ReviewDb update for change {};"
+            logger.atWarning().log(
+                "Rebuilding detected a conflicting ReviewDb update for change %s;"
                     + " will be auto-rebuilt at runtime",
                 changeId);
           } catch (Throwable t) {
-            log.error("Failed to rebuild change " + changeId, t);
+            logger.atSevere().withCause(t).log("Failed to rebuild change %s", changeId);
             ok = false;
           }
           pm.update(1);
@@ -918,19 +917,19 @@
         // to specify the repo name in the task text.
         pm.update(toSave);
       } catch (LockFailureException e) {
-        log.warn(
+        logger.atWarning().log(
             "Rebuilding detected a conflicting NoteDb update for the following refs, which will"
-                + " be auto-rebuilt at runtime: {}",
+                + " be auto-rebuilt at runtime: %s",
             e.getFailedRefs().stream().distinct().sorted().collect(joining(", ")));
       } catch (IOException e) {
-        log.error("Failed to save NoteDb state for " + project, e);
+        logger.atSevere().withCause(e).log("Failed to save NoteDb state for %s", project);
       } finally {
         pm.endTask();
       }
     } catch (RepositoryNotFoundException e) {
-      log.warn("Repository {} not found", project);
+      logger.atWarning().log("Repository %s not found", project);
     } catch (IOException e) {
-      log.error("Failed to rebuild project " + project, e);
+      logger.atSevere().withCause(e).log("Failed to rebuild project %s", project);
     }
     return ok;
   }
@@ -977,7 +976,7 @@
     try {
       return Futures.allAsList(futures).get().stream().allMatch(b -> b);
     } catch (InterruptedException | ExecutionException e) {
-      log.error(errMsg, e);
+      logger.atSevere().withCause(e).log(errMsg);
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
index 65755ed..b5a8236 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb.rebuild;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -27,12 +28,10 @@
 import com.google.inject.name.Names;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class OnlineNoteDbMigrator implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(OnlineNoteDbMigrator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String TRIAL = "OnlineNoteDbMigrator/trial";
 
@@ -79,9 +78,10 @@
   }
 
   private void migrate() {
-    log.info("Starting online NoteDb migration");
+    logger.atInfo().log("Starting online NoteDb migration");
     if (upgradeIndex) {
-      log.info("Online index schema upgrades will be deferred until NoteDb migration is complete");
+      logger.atInfo().log(
+          "Online index schema upgrades will be deferred until NoteDb migration is complete");
     }
     Stopwatch sw = Stopwatch.createStarted();
     // TODO(dborowitz): Tune threads, maybe expose a progress monitor somewhere.
@@ -89,13 +89,13 @@
         migratorBuilderProvider.get().setAutoMigrate(true).setTrialMode(trial).build()) {
       migrator.migrate();
     } catch (Exception e) {
-      log.error("Error in online NoteDb migration", e);
+      logger.atSevere().withCause(e).log("Error in online NoteDb migration");
     }
     gcAllUsers.runWithLogger();
-    log.info("Online NoteDb migration completed in {}s", sw.elapsed(TimeUnit.SECONDS));
+    logger.atInfo().log("Online NoteDb migration completed in %ss", sw.elapsed(TimeUnit.SECONDS));
 
     if (upgradeIndex) {
-      log.info("Starting deferred index schema upgrades");
+      logger.atInfo().log("Starting deferred index schema upgrades");
       indexUpgrader.start();
     }
   }
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index e4c207b..f1b6639 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -49,11 +50,9 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.TemporaryBuffer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class AutoMerger {
-  private static final Logger log = LoggerFactory.getLogger(AutoMerger.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static boolean cacheAutomerge(Config cfg) {
     return cfg.getBoolean("change", null, "cacheAutomerge", true);
@@ -119,7 +118,7 @@
       // an exception most likely means that the merge tree was not created
       // and m.getMergeResults() is empty. This would mean that all paths are
       // unmerged and Gerrit UI would show all paths in the patch list.
-      log.warn("Error attempting automerge " + refName, e);
+      logger.atWarning().withCause(e).log("Error attempting automerge %s", refName);
       return null;
     }
 
diff --git a/java/com/google/gerrit/server/patch/IntraLineLoader.java b/java/com/google/gerrit/server/patch/IntraLineLoader.java
index f17f0b6..022fd9e 100644
--- a/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -35,11 +36,9 @@
 import org.eclipse.jgit.diff.MyersDiff;
 import org.eclipse.jgit.diff.ReplaceEdit;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class IntraLineLoader implements Callable<IntraLineDiff> {
-  static final Logger log = LoggerFactory.getLogger(IntraLineLoader.class);
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   interface Factory {
     IntraLineLoader create(IntraLineDiffKey key, IntraLineDiffArgs args);
@@ -84,19 +83,15 @@
     try {
       return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | TimeoutException e) {
-      log.warn(
-          timeoutMillis
-              + " ms timeout reached for IntraLineDiff"
-              + " in project "
-              + args.project()
-              + " on commit "
-              + args.commit().name()
-              + " for path "
-              + args.path()
-              + " comparing "
-              + key.getBlobA().name()
-              + ".."
-              + key.getBlobB().name());
+      logger.atWarning().log(
+          "%s ms timeout reached for IntraLineDiff"
+              + " in project %s on commit %s for path %s comparing %s..%s",
+          timeoutMillis,
+          args.project(),
+          args.commit().name(),
+          args.path(),
+          key.getBlobA().name(),
+          key.getBlobB().name());
       result.cancel(true);
       return new IntraLineDiff(IntraLineDiff.Status.TIMEOUT);
     } catch (ExecutionException e) {
diff --git a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 01c8b41..6039fff 100644
--- a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -106,14 +106,14 @@
       }
       return pl;
     } catch (ExecutionException e) {
-      PatchListLoader.log.warn("Error computing " + key, e);
+      PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
       throw new PatchListNotAvailableException(e);
     } catch (UncheckedExecutionException e) {
       if (e.getCause() instanceof LargeObjectException) {
         // Cache negative result so we don't need to redo expensive computations that would yield
         // the same result.
         fileCache.put(key, new LargeObjectTombstone());
-        PatchListLoader.log.warn("Error computing " + key, e);
+        PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
         throw new PatchListNotAvailableException(e);
       }
       throw e;
@@ -151,7 +151,7 @@
       try {
         return intraCache.get(key, intraLoaderFactory.create(key, args));
       } catch (ExecutionException | LargeObjectException e) {
-        IntraLineLoader.log.warn("Error computing " + key, e);
+        IntraLineLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
         return new IntraLineDiff(IntraLineDiff.Status.ERROR);
       }
     }
@@ -164,11 +164,11 @@
     try {
       return diffSummaryCache.get(key, diffSummaryLoaderFactory.create(key, project));
     } catch (ExecutionException e) {
-      PatchListLoader.log.warn("Error computing " + key, e);
+      PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
       throw new PatchListNotAvailableException(e);
     } catch (UncheckedExecutionException e) {
       if (e.getCause() instanceof LargeObjectException) {
-        PatchListLoader.log.warn("Error computing " + key, e);
+        PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
         throw new PatchListNotAvailableException(e);
       }
       throw e;
diff --git a/java/com/google/gerrit/server/patch/PatchListLoader.java b/java/com/google/gerrit/server/patch/PatchListLoader.java
index 3b75f5b..8301ee6 100644
--- a/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -74,11 +75,9 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class PatchListLoader implements Callable<PatchList> {
-  static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     PatchListLoader create(PatchListKey key, Project.NameKey project);
@@ -449,19 +448,15 @@
     try {
       return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | TimeoutException e) {
-      log.warn(
-          timeoutMillis
-              + " ms timeout reached for Diff loader"
-              + " in project "
-              + project
-              + " on commit "
-              + commitB.name()
-              + " on path "
-              + diffEntry.getNewPath()
-              + " comparing "
-              + diffEntry.getOldId().name()
-              + ".."
-              + diffEntry.getNewId().name());
+      logger.atWarning().log(
+          "%s ms timeout reached for Diff loader in project %s"
+              + " on commit %s on path %s comparing %s..%s",
+          timeoutMillis,
+          project,
+          commitB.name(),
+          diffEntry.getNewPath(),
+          diffEntry.getOldId().name(),
+          diffEntry.getNewId().name());
       result.cancel(true);
       synchronized (diffEntry) {
         return toFileHeaderWithoutMyersDiff(diffFormatter, diffEntry);
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index b0e5310..b1e0e3c 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
@@ -57,10 +58,10 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class PatchScriptFactory implements Callable<PatchScript> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public interface Factory {
     PatchScriptFactory create(
         ChangeNotes notes,
@@ -77,8 +78,6 @@
         DiffPreferencesInfo diffPrefs);
   }
 
-  private static final Logger log = LoggerFactory.getLogger(PatchScriptFactory.class);
-
   private final GitRepositoryManager repoManager;
   private final PatchSetUtil psUtil;
   private final Provider<PatchScriptBuilder> builderFactory;
@@ -232,16 +231,16 @@
       } catch (PatchListNotAvailableException e) {
         throw new NoSuchChangeException(changeId, e);
       } catch (IOException e) {
-        log.error("File content unavailable", e);
+        logger.atSevere().withCause(e).log("File content unavailable");
         throw new NoSuchChangeException(changeId, e);
       } catch (org.eclipse.jgit.errors.LargeObjectException err) {
         throw new LargeObjectException("File content is too large", err);
       }
     } catch (RepositoryNotFoundException e) {
-      log.error("Repository " + notes.getProjectName() + " not found", e);
+      logger.atSevere().withCause(e).log("Repository %s not found", notes.getProjectName());
       throw new NoSuchChangeException(changeId, e);
     } catch (IOException e) {
-      log.error("Cannot open repository " + notes.getProjectName(), e);
+      logger.atSevere().withCause(e).log("Cannot open repository %s", notes.getProjectName());
       throw new NoSuchChangeException(changeId, e);
     }
   }
@@ -277,7 +276,7 @@
     try {
       return ObjectId.fromString(ps.getRevision().get());
     } catch (IllegalArgumentException e) {
-      log.error("Patch set " + ps.getId() + " has invalid revision");
+      logger.atSevere().log("Patch set %s has invalid revision", ps.getId());
       throw new NoSuchChangeException(changeId, e);
     }
   }
diff --git a/java/com/google/gerrit/server/patch/Text.java b/java/com/google/gerrit/server/patch/Text.java
index 90141715..172dbaf 100644
--- a/java/com/google/gerrit/server/patch/Text.java
+++ b/java/com/google/gerrit/server/patch/Text.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.IllegalCharsetNameException;
@@ -34,11 +35,10 @@
 import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.mozilla.universalchardet.UniversalDetector;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class Text extends RawText {
-  private static final Logger log = LoggerFactory.getLogger(Text.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final int bigFileThreshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
 
   public static final byte[] NO_BYTES = {};
@@ -157,11 +157,11 @@
       return Charset.forName(encoding);
 
     } catch (IllegalCharsetNameException err) {
-      log.error("Invalid detected charset name '" + encoding + "': " + err);
+      logger.atSevere().log("Invalid detected charset name '%s': %s", encoding, err);
       return ISO_8859_1;
 
     } catch (UnsupportedCharsetException err) {
-      log.error("Detected charset '" + encoding + "' not supported: " + err);
+      logger.atSevere().log("Detected charset '%s' not supported: %s", encoding, err);
       return ISO_8859_1;
     }
   }
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 9661156..3b88080 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -22,6 +22,7 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -60,11 +61,9 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class DefaultRefFilter {
-  private static final Logger log = LoggerFactory.getLogger(DefaultRefFilter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   interface Factory {
     DefaultRefFilter create(ProjectControl projectControl);
@@ -286,7 +285,8 @@
       } catch (AuthException e) {
         return false;
       } catch (PermissionBackendException e) {
-        log.error("Failed to check permission for " + id + " in " + projectState.getName(), e);
+        logger.atSevere().withCause(e).log(
+            "Failed to check permission for %s in %s", id, projectState.getName());
         return false;
       }
     }
@@ -306,8 +306,8 @@
       }
       return visibleChanges;
     } catch (OrmException | PermissionBackendException e) {
-      log.error(
-          "Cannot load changes for project " + project + ", assuming no changes are visible", e);
+      logger.atSevere().withCause(e).log(
+          "Cannot load changes for project %s, assuming no changes are visible", project);
       return Collections.emptyMap();
     }
   }
@@ -318,7 +318,8 @@
     try {
       s = changeNotesFactory.scan(repo, db.get(), p);
     } catch (IOException e) {
-      log.error("Cannot load changes for project " + p + ", assuming no changes are visible", e);
+      logger.atSevere().withCause(e).log(
+          "Cannot load changes for project %s, assuming no changes are visible", p);
       return Collections.emptyMap();
     }
     return s.map(this::toNotes)
@@ -329,8 +330,8 @@
   @Nullable
   private ChangeNotes toNotes(ChangeNotesResult r) {
     if (r.error().isPresent()) {
-      log.warn(
-          "Failed to load change " + r.id() + " in " + projectState.getName(), r.error().get());
+      logger.atWarning().withCause(r.error().get()).log(
+          "Failed to load change %s in %s", r.id(), projectState.getName());
       return null;
     }
     try {
@@ -339,7 +340,8 @@
         return r.notes();
       }
     } catch (PermissionBackendException e) {
-      log.error("Failed to check permission for " + r.id() + " in " + projectState.getName(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check permission for %s in %s", r.id(), projectState.getName());
     }
     return null;
   }
@@ -362,7 +364,7 @@
     } catch (AuthException e) {
       return false;
     } catch (PermissionBackendException e) {
-      log.error("unable to check permissions", e);
+      logger.atSevere().withCause(e).log("unable to check permissions");
       return false;
     }
     return projectState.statePermitsRead();
@@ -375,10 +377,8 @@
     } catch (AuthException e) {
       return false;
     } catch (PermissionBackendException e) {
-      log.error(
-          String.format(
-              "Can't check permission for user %s on project %s", user, projectState.getName()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Can't check permission for user %s on project %s", user, projectState.getName());
       return false;
     }
     return true;
diff --git a/java/com/google/gerrit/server/permissions/GlobalPermission.java b/java/com/google/gerrit/server/permissions/GlobalPermission.java
index a789bd9..71718fb 100644
--- a/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalPermission;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.CapabilityScope;
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
@@ -28,8 +29,6 @@
 import java.util.LinkedHashSet;
 import java.util.Optional;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Global server permissions built into Gerrit. */
 public enum GlobalPermission implements GlobalOrPluginPermission {
@@ -53,7 +52,7 @@
   VIEW_QUEUE,
   VIEW_ACCESS;
 
-  private static final Logger log = LoggerFactory.getLogger(GlobalPermission.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /**
    * Extracts the {@code @RequiresCapability} or {@code @RequiresAnyCapability} annotation.
@@ -70,12 +69,11 @@
     RequiresCapability rc = findAnnotation(clazz, RequiresCapability.class);
     RequiresAnyCapability rac = findAnnotation(clazz, RequiresAnyCapability.class);
     if (rc != null && rac != null) {
-      log.error(
-          String.format(
-              "Class %s uses both @%s and @%s",
-              clazz.getName(),
-              RequiresCapability.class.getSimpleName(),
-              RequiresAnyCapability.class.getSimpleName()));
+      logger.atSevere().log(
+          "Class %s uses both @%s and @%s",
+          clazz.getName(),
+          RequiresCapability.class.getSimpleName(),
+          RequiresAnyCapability.class.getSimpleName());
       throw new PermissionBackendException("cannot extract permission");
     } else if (rc != null) {
       return Collections.singleton(
@@ -124,17 +122,15 @@
     }
 
     if (scope == CapabilityScope.PLUGIN) {
-      log.error(
-          String.format(
-              "Class %s uses @%s(scope=%s), but is not within a plugin",
-              clazz.getName(), annotationClass.getSimpleName(), scope.name()));
+      logger.atSevere().log(
+          "Class %s uses @%s(scope=%s), but is not within a plugin",
+          clazz.getName(), annotationClass.getSimpleName(), scope.name());
       throw new PermissionBackendException("cannot extract permission");
     }
 
     Optional<GlobalPermission> perm = globalPermission(capability);
     if (!perm.isPresent()) {
-      log.error(
-          String.format("Class %s requires unknown capability %s", clazz.getName(), capability));
+      logger.atSevere().log("Class %s requires unknown capability %s", clazz.getName(), capability);
       throw new PermissionBackendException("cannot extract permission");
     }
     return perm.get();
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 8cdb61d..357770d 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -19,6 +19,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
@@ -44,8 +45,6 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Checks authorization to perform an action on a project, reference, or change.
@@ -91,7 +90,7 @@
  */
 @ImplementedBy(DefaultPermissionBackend.class)
 public abstract class PermissionBackend {
-  private static final Logger logger = LoggerFactory.getLogger(PermissionBackend.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Returns an instance scoped to the current user. */
   public abstract WithUser currentUser();
@@ -253,7 +252,7 @@
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
-        logger.warn("Cannot test " + perm + "; assuming false", e);
+        logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
         return false;
       }
     }
@@ -283,7 +282,8 @@
           // Do not include this project in allowed.
         } catch (PermissionBackendException e) {
           if (e.getCause() instanceof RepositoryNotFoundException) {
-            logger.warn("Could not find repository of the project {} : ", project.get(), e);
+            logger.atWarning().withCause(e).log(
+                "Could not find repository of the project %s", project.get());
             // Do not include this project because doesn't exist
           } else {
             throw e;
@@ -350,7 +350,7 @@
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
-        logger.warn("Cannot test " + perm + "; assuming false", e);
+        logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
         return false;
       }
     }
@@ -456,7 +456,7 @@
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
-        logger.warn("Cannot test " + perm + "; assuming false", e);
+        logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
         return false;
       }
     }
@@ -506,7 +506,7 @@
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
-        logger.warn("Cannot test " + perm + "; assuming false", e);
+        logger.atWarning().withCause(e).log("Cannot test %s; assuming false", perm);
         return false;
       }
     }
diff --git a/java/com/google/gerrit/server/permissions/SectionSortCache.java b/java/com/google/gerrit/server/permissions/SectionSortCache.java
index 18aea29..48c8bff 100644
--- a/java/com/google/gerrit/server/permissions/SectionSortCache.java
+++ b/java/com/google/gerrit/server/permissions/SectionSortCache.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.util.MostSpecificComparator;
@@ -28,8 +29,6 @@
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Caches the order AccessSections should be sorted for evaluation.
@@ -41,7 +40,7 @@
  */
 @Singleton
 public class SectionSortCache {
-  private static final Logger log = LoggerFactory.getLogger(SectionSortCache.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String CACHE_NAME = "permission_sort";
 
@@ -102,7 +101,7 @@
       }
 
       if (poison) {
-        log.error("Received duplicate AccessSection instances, not caching sort");
+        logger.atSevere().log("Received duplicate AccessSection instances, not caching sort");
       } else {
         cache.put(key, new EntryVal(srcIdx));
       }
diff --git a/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index da3abce..fde61ff 100644
--- a/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.annotations.Listen;
@@ -38,11 +39,9 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class AutoRegisterModules {
-  private static final Logger log = LoggerFactory.getLogger(AutoRegisterModules.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final String pluginName;
   private final PluginGuiceEnvironment env;
@@ -134,12 +133,10 @@
         initJs = STATIC_INIT_JS;
       }
     } catch (IOException e) {
-      log.warn(
-          String.format(
-              "Cannot access %s from plugin %s: "
-                  + "JavaScript auto-discovered plugin will not be registered",
-              STATIC_INIT_JS, pluginName),
-          e);
+      logger.atWarning().withCause(e).log(
+          "Cannot access %s from plugin %s: "
+              + "JavaScript auto-discovered plugin will not be registered",
+          STATIC_INIT_JS, pluginName);
     }
   }
 
@@ -155,10 +152,9 @@
 
     Export export = clazz.getAnnotation(Export.class);
     if (export == null) {
-      log.warn(
-          String.format(
-              "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
-              pluginName, clazz.getName(), def.annotationValue));
+      logger.atWarning().log(
+          "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
+          pluginName, clazz.getName(), def.annotationValue);
       return;
     }
 
@@ -192,9 +188,8 @@
     if (listen != null) {
       listen(clazz, clazz);
     } else {
-      log.warn(
-          String.format(
-              "In plugin %s asm incorrectly parsed %s with @Listen", pluginName, clazz.getName()));
+      logger.atWarning().log(
+          "In plugin %s asm incorrectly parsed %s with @Listen", pluginName, clazz.getName());
     }
   }
 
diff --git a/java/com/google/gerrit/server/plugins/CleanupHandle.java b/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 5a60ee2..87d6bb0 100644
--- a/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -14,15 +14,14 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.jar.JarFile;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class CleanupHandle {
-  private static final Logger log = LoggerFactory.getLogger(CleanupHandle.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Path tmp;
   private final JarFile jarFile;
@@ -36,17 +35,15 @@
     try {
       jarFile.close();
     } catch (IOException err) {
-      log.error("Cannot close " + jarFile.getName(), err);
+      logger.atSevere().withCause(err).log("Cannot close %s", jarFile.getName());
     }
     try {
       Files.deleteIfExists(tmp);
-      log.info("Cleaned plugin " + tmp.getFileName());
+      logger.atInfo().log("Cleaned plugin %s", tmp.getFileName());
     } catch (IOException e) {
-      log.warn(
-          "Cannot delete "
-              + tmp.toAbsolutePath()
-              + ", retrying to delete it on termination of the virtual machine",
-          e);
+      logger.atWarning().withCause(e).log(
+          "Cannot delete %s, retrying to delete it on termination of the virtual machine",
+          tmp.toAbsolutePath());
       tmp.toFile().deleteOnExit();
     }
   }
diff --git a/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 87c3df7..229f394 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.SitePaths;
@@ -34,13 +35,12 @@
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class JarPluginProvider implements ServerPluginProvider {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final String JAR_EXTENSION = ".jar";
-  static final Logger log = LoggerFactory.getLogger(JarPluginProvider.class);
 
   private final Path tmpDir;
   private final PluginConfigFactory configFactory;
@@ -129,7 +129,7 @@
       if (overlay != null) {
         Path classes = Paths.get(overlay).resolve(name).resolve("main");
         if (Files.isDirectory(classes)) {
-          log.info(String.format("plugin %s: including %s", name, classes));
+          logger.atInfo().log("plugin %s: including %s", name, classes);
           urls.add(classes.toUri().toURL());
         }
       }
@@ -152,7 +152,8 @@
               jarScanner,
               description.dataDir,
               pluginLoader,
-              pluginConfig.getString("metricsPrefix", null));
+              pluginConfig.getString("metricsPrefix", null),
+              description.gerritRuntime);
       plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
       keep = true;
       return plugin;
diff --git a/java/com/google/gerrit/server/plugins/JarScanner.java b/java/com/google/gerrit/server/plugins/JarScanner.java
index 1310b8c..1a9b859 100644
--- a/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
@@ -50,11 +51,10 @@
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class JarScanner implements PluginContentScanner, AutoCloseable {
-  private static final Logger log = LoggerFactory.getLogger(JarScanner.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final int SKIP_ALL =
       ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
   private final JarFile jarFile;
@@ -91,11 +91,9 @@
       } catch (IOException err) {
         throw new InvalidPluginException("Cannot auto-register", err);
       } catch (RuntimeException err) {
-        log.warn(
-            String.format(
-                "Plugin %s has invalid class file %s inside of %s",
-                pluginName, entry.getName(), jarFile.getName()),
-            err);
+        logger.atWarning().withCause(err).log(
+            "Plugin %s has invalid class file %s inside of %s",
+            pluginName, entry.getName(), jarFile.getName());
         continue;
       }
 
@@ -103,10 +101,9 @@
         if (def.isConcrete()) {
           rawMap.put(def.annotationName, def);
         } else {
-          log.warn(
-              String.format(
-                  "Plugin %s tries to @%s(\"%s\") abstract class %s",
-                  pluginName, def.annotationName, def.annotationValue, def.className));
+          logger.atWarning().log(
+              "Plugin %s tries to @%s(\"%s\") abstract class %s",
+              pluginName, def.annotationName, def.annotationValue, def.className);
         }
       }
     }
@@ -151,9 +148,8 @@
       try {
         new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL);
       } catch (RuntimeException err) {
-        log.warn(
-            String.format("Jar %s has invalid class file %s", jarFile.getName(), entry.getName()),
-            err);
+        logger.atWarning().withCause(err).log(
+            "Jar %s has invalid class file %s", jarFile.getName(), entry.getName());
         continue;
       }
 
diff --git a/java/com/google/gerrit/server/plugins/ListPlugins.java b/java/com/google/gerrit/server/plugins/ListPlugins.java
index 03c02cf..84e63d0 100644
--- a/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -47,50 +47,45 @@
   private String matchRegex;
 
   @Option(
-    name = "--all",
-    aliases = {"-a"},
-    usage = "List all plugins, including disabled plugins"
-  )
+      name = "--all",
+      aliases = {"-a"},
+      usage = "List all plugins, including disabled plugins")
   public void setAll(boolean all) {
     this.all = all;
   }
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of plugins to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of plugins to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "number of plugins to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "number of plugins to skip")
   public void setStart(int start) {
     this.start = start;
   }
 
   @Option(
-    name = "--prefix",
-    aliases = {"-p"},
-    metaVar = "PREFIX",
-    usage = "match plugin prefix"
-  )
+      name = "--prefix",
+      aliases = {"-p"},
+      metaVar = "PREFIX",
+      usage = "match plugin prefix")
   public void setMatchPrefix(String matchPrefix) {
     this.matchPrefix = matchPrefix;
   }
 
   @Option(
-    name = "--match",
-    aliases = {"-m"},
-    metaVar = "MATCH",
-    usage = "match plugin substring"
-  )
+      name = "--match",
+      aliases = {"-m"},
+      metaVar = "MATCH",
+      usage = "match plugin substring")
   public void setMatchSubstring(String matchSubstring) {
     this.matchSubstring = matchSubstring;
   }
diff --git a/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
index 5cff345..11a4eab 100644
--- a/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
+++ b/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class PluginCleanerTask implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(PluginCleanerTask.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final WorkQueue workQueue;
   private final PluginLoader loader;
@@ -58,10 +57,9 @@
 
       if (0 < left) {
         long waiting = TimeUtil.nowMs() - start;
-        log.warn(
-            String.format(
-                "%d plugins still waiting to be reclaimed after %d minutes",
-                pending, TimeUnit.MILLISECONDS.toMinutes(waiting)));
+        logger.atWarning().log(
+            "%d plugins still waiting to be reclaimed after %d minutes",
+            pending, TimeUnit.MILLISECONDS.toMinutes(waiting));
         attempts = Math.min(attempts + 1, 15);
         ensureScheduled();
       } else {
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 07ffbdc..57e7e49 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -33,6 +34,7 @@
 import com.google.gerrit.server.cache.PersistentCacheFactory;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription;
@@ -61,12 +63,10 @@
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class PluginLoader implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public String getPluginName(Path srcPath) {
     return MoreObjects.firstNonNull(getGerritPluginName(srcPath), PluginUtil.nameOf(srcPath));
@@ -89,6 +89,7 @@
   private final PersistentCacheFactory persistentCacheFactory;
   private final boolean remoteAdmin;
   private final UniversalServerPluginProvider serverPluginFactory;
+  private final GerritRuntime gerritRuntime;
 
   @Inject
   public PluginLoader(
@@ -100,7 +101,8 @@
       @GerritServerConfig Config cfg,
       @CanonicalWebUrl Provider<String> provider,
       PersistentCacheFactory cacheFactory,
-      UniversalServerPluginProvider pluginFactory) {
+      UniversalServerPluginProvider pluginFactory,
+      GerritRuntime gerritRuntime) {
     pluginsDir = sitePaths.plugins_dir;
     dataDir = sitePaths.data_dir;
     tempDir = sitePaths.tmp_dir;
@@ -113,6 +115,7 @@
     serverPluginFactory = pluginFactory;
 
     remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
+    this.gerritRuntime = gerritRuntime;
 
     long checkFrequency =
         ConfigUtil.getTimeUnit(
@@ -164,10 +167,9 @@
     Path tmp = PluginUtil.asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
     String name = MoreObjects.firstNonNull(getGerritPluginName(tmp), PluginUtil.nameOf(fileName));
     if (!originalName.equals(name)) {
-      log.warn(
-          String.format(
-              "Plugin provides its own name: <%s>, use it instead of the input name: <%s>",
-              name, originalName));
+      logger.atWarning().log(
+          "Plugin provides its own name: <%s>, use it instead of the input name: <%s>",
+          name, originalName);
     }
 
     String fileExtension = getExtension(fileName);
@@ -176,7 +178,7 @@
       Plugin active = running.get(name);
       if (active != null) {
         fileName = active.getSrcFile().getFileName().toString();
-        log.info(String.format("Replacing plugin %s", active.getName()));
+        logger.atInfo().log("Replacing plugin %s", active.getName());
         Path old = pluginsDir.resolve(".last_" + fileName);
         Files.deleteIfExists(old);
         Files.move(active.getSrcFile(), old);
@@ -187,7 +189,7 @@
       try {
         Plugin plugin = runPlugin(name, dst, active);
         if (active == null) {
-          log.info(String.format("Installed plugin %s", plugin.getName()));
+          logger.atInfo().log("Installed plugin %s", plugin.getName());
         }
       } catch (PluginInstallException e) {
         Files.deleteIfExists(dst);
@@ -203,7 +205,7 @@
   private synchronized void unloadPlugin(Plugin plugin) {
     persistentCacheFactory.onStop(plugin.getName());
     String name = plugin.getName();
-    log.info(String.format("Unloading plugin %s, version %s", name, plugin.getVersion()));
+    logger.atInfo().log("Unloading plugin %s, version %s", name, plugin.getVersion());
     plugin.stop(env);
     env.onStopPlugin(plugin);
     running.remove(name);
@@ -213,7 +215,8 @@
 
   public void disablePlugins(Set<String> names) {
     if (!isRemoteAdminEnabled()) {
-      log.warn("Remote plugin administration is disabled, ignoring disablePlugins(" + names + ")");
+      logger.atWarning().log(
+          "Remote plugin administration is disabled, ignoring disablePlugins(%s)", names);
       return;
     }
 
@@ -224,13 +227,13 @@
           continue;
         }
 
-        log.info(String.format("Disabling plugin %s", active.getName()));
+        logger.atInfo().log("Disabling plugin %s", active.getName());
         Path off =
             active.getSrcFile().resolveSibling(active.getSrcFile().getFileName() + ".disabled");
         try {
           Files.move(active.getSrcFile(), off);
         } catch (IOException e) {
-          log.error("Failed to disable plugin", e);
+          logger.atSevere().withCause(e).log("Failed to disable plugin");
           // In theory we could still unload the plugin even if the rename
           // failed. However, it would be reloaded on the next server startup,
           // which is probably not what the user expects.
@@ -244,7 +247,8 @@
           disabled.put(name, offPlugin);
         } catch (Throwable e) {
           // This shouldn't happen, as the plugin was loaded earlier.
-          log.warn(String.format("Cannot load disabled plugin %s", active.getName()), e.getCause());
+          logger.atWarning().withCause(e.getCause()).log(
+              "Cannot load disabled plugin %s", active.getName());
         }
       }
       cleanInBackground();
@@ -253,7 +257,8 @@
 
   public void enablePlugins(Set<String> names) throws PluginInstallException {
     if (!isRemoteAdminEnabled()) {
-      log.warn("Remote plugin administration is disabled, ignoring enablePlugins(" + names + ")");
+      logger.atWarning().log(
+          "Remote plugin administration is disabled, ignoring enablePlugins(%s)", names);
       return;
     }
 
@@ -264,7 +269,7 @@
           continue;
         }
 
-        log.info(String.format("Enabling plugin %s", name));
+        logger.atInfo().log("Enabling plugin %s", name);
         String n = off.getSrcFile().toFile().getName();
         if (n.endsWith(".disabled")) {
           n = n.substring(0, n.lastIndexOf('.'));
@@ -273,7 +278,7 @@
         try {
           Files.move(off.getSrcFile(), on);
         } catch (IOException e) {
-          log.error("Failed to move plugin " + name + " into place", e);
+          logger.atSevere().withCause(e).log("Failed to move plugin %s into place", name);
           continue;
         }
         disabled.remove(name);
@@ -293,18 +298,16 @@
         };
     try (DirectoryStream<Path> files = Files.newDirectoryStream(tempDir, filter)) {
       for (Path file : files) {
-        log.info("Removing stale plugin file: " + file.toFile().getName());
+        logger.atInfo().log("Removing stale plugin file: %s", file.toFile().getName());
         try {
           Files.delete(file);
         } catch (IOException e) {
-          log.error(
-              String.format(
-                  "Failed to remove stale plugin file %s: %s",
-                  file.toFile().getName(), e.getMessage()));
+          logger.atSevere().log(
+              "Failed to remove stale plugin file %s: %s", file.toFile().getName(), e.getMessage());
         }
       }
     } catch (IOException e) {
-      log.warn("Unable to discover stale plugin files: " + e.getMessage());
+      logger.atWarning().log("Unable to discover stale plugin files: %s", e.getMessage());
     }
   }
 
@@ -313,14 +316,14 @@
     removeStalePluginFiles();
     Path absolutePath = pluginsDir.toAbsolutePath();
     if (!Files.exists(absolutePath)) {
-      log.info(absolutePath + " does not exist; creating");
+      logger.atInfo().log("%s does not exist; creating", absolutePath);
       try {
         Files.createDirectories(absolutePath);
       } catch (IOException e) {
-        log.error(String.format("Failed to create %s: %s", absolutePath, e.getMessage()));
+        logger.atSevere().log("Failed to create %s: %s", absolutePath, e.getMessage());
       }
     }
-    log.info("Loading plugins from " + absolutePath);
+    logger.atInfo().log("Loading plugins from %s", absolutePath);
     srvInfoImpl.state = ServerInformation.State.STARTUP;
     rescan();
     srvInfoImpl.state = ServerInformation.State.RUNNING;
@@ -369,13 +372,12 @@
       for (Plugin active : reload) {
         String name = active.getName();
         try {
-          log.info(String.format("Reloading plugin %s", name));
+          logger.atInfo().log("Reloading plugin %s", name);
           Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
-          log.info(
-              String.format(
-                  "Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion()));
+          logger.atInfo().log(
+              "Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion());
         } catch (PluginInstallException e) {
-          log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
+          logger.atWarning().withCause(e.getCause()).log("Cannot reload plugin %s", name);
           throw e;
         }
       }
@@ -398,7 +400,8 @@
       Path path = entry.getValue();
       String fileName = path.getFileName().toString();
       if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
-        log.warn("No Plugin provider was found that handles this file format: {}", fileName);
+        logger.atWarning().log(
+            "No Plugin provider was found that handles this file format: %s", fileName);
         continue;
       }
 
@@ -413,21 +416,20 @@
       }
 
       if (active != null) {
-        log.info(String.format("Reloading plugin %s", active.getName()));
+        logger.atInfo().log("Reloading plugin %s", active.getName());
       }
 
       try {
         Plugin loadedPlugin = runPlugin(name, path, active);
         if (!loadedPlugin.isDisabled()) {
-          log.info(
-              String.format(
-                  "%s plugin %s, version %s",
-                  active == null ? "Loaded" : "Reloaded",
-                  loadedPlugin.getName(),
-                  loadedPlugin.getVersion()));
+          logger.atInfo().log(
+              "%s plugin %s, version %s",
+              active == null ? "Loaded" : "Reloaded",
+              loadedPlugin.getName(),
+              loadedPlugin.getVersion());
         }
       } catch (PluginInstallException e) {
-        log.warn(String.format("Cannot load plugin %s", name), e.getCause());
+        logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
       }
     }
 
@@ -611,7 +613,8 @@
         new PluginDescription(
             pluginUserFactory.create(name),
             getPluginCanonicalWebUrl(name),
-            getPluginDataDir(name)));
+            getPluginDataDir(name),
+            gerritRuntime));
   }
 
   // Only one active plugin per plugin name can exist for each plugin name.
@@ -660,19 +663,18 @@
       Collection<Path> elementsToRemove = new ArrayList<>();
       Collection<Path> elementsToAdd = new ArrayList<>();
       for (Path loser : Iterables.skip(enabled, 1)) {
-        log.warn(
-            String.format(
-                "Plugin <%s> was disabled, because"
-                    + " another plugin <%s>"
-                    + " with the same name <%s> already exists",
-                loser, winner, plugin));
+        logger.atWarning().log(
+            "Plugin <%s> was disabled, because"
+                + " another plugin <%s>"
+                + " with the same name <%s> already exists",
+            loser, winner, plugin);
         Path disabledPlugin = Paths.get(loser + ".disabled");
         elementsToAdd.add(disabledPlugin);
         elementsToRemove.add(loser);
         try {
           Files.move(loser, disabledPlugin);
         } catch (IOException e) {
-          log.warn("Failed to fully disable plugin " + loser, e);
+          logger.atWarning().withCause(e).log("Failed to fully disable plugin %s", loser);
         }
       }
       Iterables.removeAll(files, elementsToRemove);
@@ -685,7 +687,7 @@
     try {
       return PluginUtil.listPlugins(pluginsDir);
     } catch (IOException e) {
-      log.error("Cannot list " + pluginsDir.toAbsolutePath(), e);
+      logger.atSevere().withCause(e).log("Cannot list %s", pluginsDir.toAbsolutePath());
       return ImmutableList.of();
     }
   }
diff --git a/java/com/google/gerrit/server/plugins/PluginModule.java b/java/com/google/gerrit/server/plugins/PluginModule.java
index db18470..6bc37bd 100644
--- a/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -17,10 +17,15 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.config.GerritRuntime;
 
 public class PluginModule extends LifecycleModule {
   @Override
   protected void configure() {
+    requireBinding(GerritRuntime.class);
+
+    factory(PluginUser.Factory.class);
     bind(ServerInformationImpl.class);
     bind(ServerInformation.class).to(ServerInformationImpl.class);
 
diff --git a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
index 8e162ba..cad0e1e 100644
--- a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
+++ b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
@@ -18,10 +18,13 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.PluginUser;
+import com.google.inject.Key;
 
 public class PluginRestApiModule extends RestApiModule {
   @Override
   protected void configure() {
+    requireBinding(Key.get(PluginUser.Factory.class));
     bind(PluginsCollection.class);
     DynamicMap.mapOf(binder(), PLUGIN_KIND);
     put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index b46ecf5..6ae00e1 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -16,11 +16,13 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -32,11 +34,9 @@
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ServerPlugin extends Plugin {
-  private static final Logger log = LoggerFactory.getLogger(ServerPlugin.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Manifest manifest;
   private final PluginContentScanner scanner;
@@ -44,6 +44,7 @@
   private final String pluginCanonicalWebUrl;
   private final ClassLoader classLoader;
   private final String metricsPrefix;
+  private final GerritRuntime gerritRuntime;
   protected Class<? extends Module> sysModule;
   protected Class<? extends Module> sshModule;
   protected Class<? extends Module> httpModule;
@@ -63,7 +64,8 @@
       PluginContentScanner scanner,
       Path dataDir,
       ClassLoader classLoader,
-      String metricsPrefix)
+      String metricsPrefix,
+      GerritRuntime gerritRuntime)
       throws InvalidPluginException {
     super(
         name,
@@ -77,33 +79,12 @@
     this.classLoader = classLoader;
     this.manifest = scanner == null ? null : getPluginManifest(scanner);
     this.metricsPrefix = metricsPrefix;
+    this.gerritRuntime = gerritRuntime;
     if (manifest != null) {
       loadGuiceModules(manifest, classLoader);
     }
   }
 
-  public ServerPlugin(
-      String name,
-      String pluginCanonicalWebUrl,
-      PluginUser pluginUser,
-      Path srcJar,
-      FileSnapshot snapshot,
-      PluginContentScanner scanner,
-      Path dataDir,
-      ClassLoader classLoader)
-      throws InvalidPluginException {
-    this(
-        name,
-        pluginCanonicalWebUrl,
-        pluginUser,
-        srcJar,
-        snapshot,
-        scanner,
-        dataDir,
-        classLoader,
-        null);
-  }
-
   private void loadGuiceModules(Manifest manifest, ClassLoader classLoader)
       throws InvalidPluginException {
     Attributes main = manifest.getMainAttributes();
@@ -178,9 +159,8 @@
     } else if ("restart".equalsIgnoreCase(v)) {
       return false;
     } else {
-      log.warn(
-          String.format(
-              "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart", getName(), v));
+      logger.atWarning().log(
+          "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart", getName(), v);
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/plugins/ServerPluginProvider.java b/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
index 632f838..f2f64c2 100644
--- a/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.config.GerritRuntime;
 import java.nio.file.Path;
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
@@ -36,6 +37,7 @@
     public final PluginUser user;
     public final String canonicalUrl;
     public final Path dataDir;
+    final GerritRuntime gerritRuntime;
 
     /**
      * Creates a new PluginDescription for ServerPluginProvider.
@@ -43,11 +45,14 @@
      * @param user Gerrit user for interacting with plugins
      * @param canonicalUrl plugin root Web URL
      * @param dataDir directory for plugin data
+     * @param gerritRuntime current Gerrit runtime (daemon, batch, ...)
      */
-    public PluginDescription(PluginUser user, String canonicalUrl, Path dataDir) {
+    public PluginDescription(
+        PluginUser user, String canonicalUrl, Path dataDir, GerritRuntime gerritRuntime) {
       this.user = user;
       this.canonicalUrl = canonicalUrl;
       this.dataDir = dataDir;
+      this.gerritRuntime = gerritRuntime;
     }
   }
 
diff --git a/java/com/google/gerrit/server/plugins/TestServerPlugin.java b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
index dbdc576..3751c3f 100644
--- a/java/com/google/gerrit/server/plugins/TestServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.config.GerritRuntime;
 import java.nio.file.Path;
 
 public class TestServerPlugin extends ServerPlugin {
@@ -33,7 +34,17 @@
       String sshName,
       Path dataDir)
       throws InvalidPluginException {
-    super(name, pluginCanonicalWebUrl, user, null, null, null, dataDir, classloader);
+    super(
+        name,
+        pluginCanonicalWebUrl,
+        user,
+        null,
+        null,
+        null,
+        dataDir,
+        classloader,
+        null,
+        GerritRuntime.DAEMON);
     this.classLoader = classloader;
     this.sysName = sysName;
     this.httpName = httpName;
diff --git a/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
index 50b8752..0bef1e5 100644
--- a/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -21,12 +22,10 @@
 import java.util.ArrayList;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class UniversalServerPluginProvider implements ServerPluginProvider {
-  private static final Logger log = LoggerFactory.getLogger(UniversalServerPluginProvider.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicSet<ServerPluginProvider> serverPluginProviders;
 
@@ -82,11 +81,9 @@
     List<ServerPluginProvider> providers = new ArrayList<>();
     for (ServerPluginProvider serverPluginProvider : serverPluginProviders) {
       boolean handles = serverPluginProvider.handles(srcPath);
-      log.debug(
-          "File {} handled by {} ? => {}",
-          srcPath,
-          serverPluginProvider.getProviderPluginName(),
-          handles);
+      logger.atFine().log(
+          "File %s handled by %s ? => %s",
+          srcPath, serverPluginProvider.getProviderPluginName(), handles);
       if (handles) {
         providers.add(serverPluginProvider);
       }
diff --git a/java/com/google/gerrit/server/project/CommentLinkProvider.java b/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 516965b..56cf51e 100644
--- a/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
 import com.google.gerrit.server.config.GerritConfigListener;
@@ -27,12 +28,10 @@
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class CommentLinkProvider implements Provider<List<CommentLinkInfo>>, GerritConfigListener {
-  private static final Logger log = LoggerFactory.getLogger(CommentLinkProvider.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private volatile List<CommentLinkInfo> commentLinks;
 
@@ -48,12 +47,12 @@
       try {
         CommentLinkInfoImpl cl = ProjectConfig.buildCommentLink(cfg, name, true);
         if (cl.isOverrideOnly()) {
-          log.warn("commentlink " + name + " empty except for \"enabled\"");
+          logger.atWarning().log("commentlink %s empty except for \"enabled\"", name);
           continue;
         }
         cls.add(cl);
       } catch (IllegalArgumentException e) {
-        log.warn("invalid commentlink: " + e.getMessage());
+        logger.atWarning().log("invalid commentlink: %s", e.getMessage());
       }
     }
     return ImmutableList.copyOf(cls);
diff --git a/java/com/google/gerrit/server/project/ConfiguredMimeTypes.java b/java/com/google/gerrit/server/project/ConfiguredMimeTypes.java
index 9181e80..a6661f7 100644
--- a/java/com/google/gerrit/server/project/ConfiguredMimeTypes.java
+++ b/java/com/google/gerrit/server/project/ConfiguredMimeTypes.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.flogger.FluentLogger;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -23,11 +24,9 @@
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.fnmatch.FileNameMatcher;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ConfiguredMimeTypes {
-  private static final Logger log = LoggerFactory.getLogger(ConfiguredMimeTypes.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String MIMETYPE = "mimetype";
   private static final String KEY_PATH = "path";
@@ -45,10 +44,9 @@
           try {
             add(typeName, path);
           } catch (PatternSyntaxException | InvalidPatternException e) {
-            log.warn(
-                String.format(
-                    "Ignoring invalid %s.%s.%s = %s in project %s: %s",
-                    MIMETYPE, typeName, KEY_PATH, path, projectName, e.getMessage()));
+            logger.atWarning().log(
+                "Ignoring invalid %s.%s.%s = %s in project %s: %s",
+                MIMETYPE, typeName, KEY_PATH, path, projectName, e.getMessage());
           }
         }
       }
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 90a7455..f89e298 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -32,13 +33,11 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Manages access control for creating Git references (aka branches, tags). */
 @Singleton
 public class CreateRefControl {
-  private static final Logger log = LoggerFactory.getLogger(CreateRefControl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
@@ -85,7 +84,8 @@
       try (RevWalk rw = new RevWalk(repo)) {
         rw.parseBody(tag);
       } catch (IOException e) {
-        log.error(String.format("RevWalk(%s) parsing %s:", branch.getParentKey(), tag.name()), e);
+        logger.atSevere().withCause(e).log(
+            "RevWalk(%s) parsing %s:", branch.getParentKey(), tag.name());
         throw e;
       }
 
diff --git a/java/com/google/gerrit/server/project/GroupList.java b/java/com/google/gerrit/server/project/GroupList.java
index f70f2e6..fdb8740 100644
--- a/java/com/google/gerrit/server/project/GroupList.java
+++ b/java/com/google/gerrit/server/project/GroupList.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -26,11 +27,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class GroupList extends TabFile {
-  private static final Logger log = LoggerFactory.getLogger(GroupList.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String FILE_NAME = "groups";
 
@@ -46,7 +45,7 @@
     Map<AccountGroup.UUID, GroupReference> groupsByUUID = new HashMap<>(rows.size());
     for (Row row : rows) {
       if (row.left == null) {
-        log.warn("null field in group list for {}:\n{}", project, text);
+        logger.atWarning().log("null field in group list for %s:\n%s", project, text);
         continue;
       }
       AccountGroup.UUID uuid = new AccountGroup.UUID(row.left);
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 296965a..1f51fda 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -44,13 +45,11 @@
 import java.util.concurrent.locks.ReentrantLock;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Cache of project information, including access rights. */
 @Singleton
 public class ProjectCacheImpl implements ProjectCache {
-  private static final Logger log = LoggerFactory.getLogger(ProjectCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String CACHE_NAME = "projects";
 
@@ -132,7 +131,7 @@
     try {
       return checkedGet(projectName);
     } catch (IOException e) {
-      log.warn("Cannot read project " + projectName, e);
+      logger.atWarning().withCause(e).log("Cannot read project %s", projectName);
       return null;
     }
   }
@@ -146,11 +145,11 @@
       return strictCheckedGet(projectName);
     } catch (Exception e) {
       if (!(e.getCause() instanceof RepositoryNotFoundException)) {
-        log.warn(String.format("Cannot read project %s", projectName.get()), e);
+        logger.atWarning().withCause(e).log("Cannot read project %s", projectName.get());
         Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
         throw new IOException(e);
       }
-      log.debug("Cannot find project {}", projectName.get(), e);
+      logger.atFine().withCause(e).log("Cannot find project %s", projectName.get());
       return null;
     }
   }
@@ -195,7 +194,7 @@
           ListKey.ALL,
           ImmutableSortedSet.copyOf(Sets.difference(list.get(ListKey.ALL), ImmutableSet.of(name))));
     } catch (ExecutionException e) {
-      log.warn("Cannot list available projects", e);
+      logger.atWarning().withCause(e).log("Cannot list available projects");
     } finally {
       listLock.unlock();
     }
@@ -211,7 +210,7 @@
           ImmutableSortedSet.copyOf(
               Sets.union(list.get(ListKey.ALL), ImmutableSet.of(newProjectName))));
     } catch (ExecutionException e) {
-      log.warn("Cannot list available projects", e);
+      logger.atWarning().withCause(e).log("Cannot list available projects");
     } finally {
       listLock.unlock();
     }
@@ -223,7 +222,7 @@
     try {
       return list.get(ListKey.ALL);
     } catch (ExecutionException e) {
-      log.warn("Cannot list available projects", e);
+      logger.atWarning().withCause(e).log("Cannot list available projects");
       return ImmutableSortedSet.of();
     }
   }
@@ -249,7 +248,7 @@
       // Right endpoint is exclusive, but U+FFFF is a non-character so no project ends with it.
       return list.get(ListKey.ALL).subSet(start, end);
     } catch (ExecutionException e) {
-      log.warn("Cannot look up projects for prefix " + pfx, e);
+      logger.atWarning().withCause(e).log("Cannot look up projects for prefix %s", pfx);
       return ImmutableSortedSet.of();
     }
   }
diff --git a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index 10ab746..7ebbc51 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.reviewdb.client.Project;
@@ -24,12 +25,10 @@
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class ProjectCacheWarmer implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(ProjectCacheWarmer.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Config config;
   private final ProjectCache cache;
@@ -57,15 +56,15 @@
                 pool.shutdown();
                 try {
                   pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
-                  log.info("Finished loading project cache");
+                  logger.atInfo().log("Finished loading project cache");
                 } catch (InterruptedException e) {
-                  log.warn("Interrupted while waiting for project cache to load");
+                  logger.atWarning().log("Interrupted while waiting for project cache to load");
                 }
               });
       scheduler.setName("ProjectCacheWarmer");
       scheduler.setDaemon(true);
 
-      log.info("Loading project cache");
+      logger.atInfo().log("Loading project cache");
       scheduler.start();
     }
   }
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index b9e0266..ba1a652 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -66,6 +66,7 @@
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
@@ -77,7 +78,6 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.util.StringUtils;
 
 public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
   public static final String COMMENTLINK = "commentlink";
@@ -1179,7 +1179,7 @@
         List<String> types = new ArrayList<>(4);
         for (NotifyType t : NotifyType.values()) {
           if (nc.isNotify(t)) {
-            types.add(StringUtils.toLowerCase(t.name()));
+            types.add(t.name().toLowerCase(Locale.US));
           }
         }
         rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
diff --git a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
index ac8d536..27bde72 100644
--- a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
+++ b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -17,14 +17,13 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Iterates from a project up through its parents to All-Projects.
@@ -32,7 +31,7 @@
  * <p>If a cycle is detected the cycle is broken and All-Projects is visited.
  */
 class ProjectHierarchyIterator implements Iterator<ProjectState> {
-  private static final Logger log = LoggerFactory.getLogger(ProjectHierarchyIterator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ProjectCache cache;
   private final AllProjectsName allProjectsName;
@@ -91,8 +90,8 @@
     }
     int idx = order.lastIndexOf(parentName.get());
     order.add(parentName.get());
-    log.warn(
-        "Cycle detected in projects: " + Joiner.on(" -> ").join(order.subList(idx, order.size())));
+    logger.atWarning().log(
+        "Cycle detected in projects: %s", Joiner.on(" -> ").join(order.subList(idx, order.size())));
     return false;
   }
 
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 28f620b..a490f10 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
@@ -66,12 +67,10 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Cached information on a project. */
 public class ProjectState {
-  private static final Logger log = LoggerFactory.getLogger(ProjectState.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     ProjectState create(ProjectConfig config);
@@ -226,7 +225,7 @@
     try (Repository git = gitMgr.openRepository(getNameKey())) {
       cfg.load(git, config.getRevision());
     } catch (IOException | ConfigInvalidException e) {
-      log.warn("Failed to load " + fileName + " for " + getName(), e);
+      logger.atWarning().withCause(e).log("Failed to load %s for %s", fileName, getName());
     }
 
     configs.put(fileName, cfg);
@@ -517,7 +516,7 @@
     if (!Files.exists(dir)) {
       return ThemeInfo.INHERIT;
     } else if (!Files.isDirectory(dir)) {
-      log.warn("Bad theme for {}: not a directory", name);
+      logger.atWarning().log("Bad theme for %s: not a directory", name);
       return ThemeInfo.INHERIT;
     }
     try {
@@ -526,7 +525,7 @@
           readFile(dir.resolve(SitePaths.HEADER_FILENAME)),
           readFile(dir.resolve(SitePaths.FOOTER_FILENAME)));
     } catch (IOException e) {
-      log.error("Error reading theme for " + name, e);
+      logger.atSevere().withCause(e).log("Error reading theme for %s", name);
       return ThemeInfo.INHERIT;
     }
   }
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 0196d92..322d362e 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -32,8 +33,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Report whether a commit is reachable from a set of commits. This is used for checking if a user
@@ -41,7 +40,7 @@
  */
 @Singleton
 public class Reachable {
-  private static final Logger log = LoggerFactory.getLogger(Reachable.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
 
@@ -61,11 +60,8 @@
               .filter(refs, repo, RefFilterOptions.builder().setFilterTagsSeparately(true).build());
       return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
     } catch (IOException | PermissionBackendException e) {
-      log.error(
-          String.format(
-              "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), project),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Cannot verify permissions to commit object %s in repository %s", commit.name(), project);
       return false;
     }
   }
@@ -82,11 +78,8 @@
       }
       return fromRefs(project, repo, commit, refs);
     } catch (IOException e) {
-      log.error(
-          String.format(
-              "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), project),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Cannot verify permissions to commit object %s in repository %s", commit.name(), project);
       return false;
     }
   }
diff --git a/java/com/google/gerrit/server/project/RefUtil.java b/java/com/google/gerrit/server/project/RefUtil.java
index e42a7df..e5951a8 100644
--- a/java/com/google/gerrit/server/project/RefUtil.java
+++ b/java/com/google/gerrit/server/project/RefUtil.java
@@ -18,6 +18,7 @@
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -33,11 +34,9 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.ObjectWalk;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class RefUtil {
-  private static final Logger log = LoggerFactory.getLogger(RefUtil.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private RefUtil() {}
 
@@ -51,11 +50,11 @@
       }
       return revid;
     } catch (IOException err) {
-      log.error(
-          "Cannot resolve \"" + baseRevision + "\" in project \"" + projectName.get() + "\"", err);
+      logger.atSevere().withCause(err).log(
+          "Cannot resolve \"%s\" in project \"%s\"", baseRevision, projectName.get());
       throw new InvalidRevisionException();
     } catch (RevisionSyntaxException err) {
-      log.error("Invalid revision syntax \"" + baseRevision + "\"", err);
+      logger.atSevere().withCause(err).log("Invalid revision syntax \"%s\"", baseRevision);
       throw new InvalidRevisionException();
     }
   }
@@ -89,9 +88,8 @@
     } catch (IncorrectObjectTypeException | MissingObjectException err) {
       throw new InvalidRevisionException();
     } catch (IOException err) {
-      log.error(
-          "Repository \"" + repo.getDirectory() + "\" may be corrupt; suggest running git fsck",
-          err);
+      logger.atSevere().withCause(err).log(
+          "Repository \"%s\" may be corrupt; suggest running git fsck", repo.getDirectory());
       throw new InvalidRevisionException();
     }
   }
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index cb01b81..3fcb3a9 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -29,15 +30,14 @@
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
  * the results through rules found in the parent projects, all the way up to All-Projects.
  */
 public class SubmitRuleEvaluator {
-  private static final Logger log = LoggerFactory.getLogger(SubmitRuleEvaluator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String DEFAULT_MSG = "Error evaluating project rules, check server log";
 
   private final ProjectCache projectCache;
@@ -119,9 +119,9 @@
   private List<SubmitRecord> ruleError(String err, Exception e) {
     if (opts.logErrors()) {
       if (e == null) {
-        log.error(err);
+        logger.atSevere().log(err);
       } else {
-        log.error(err, e);
+        logger.atSevere().withCause(e).log(err);
       }
       return defaultRuleError();
     }
@@ -150,11 +150,7 @@
 
   private SubmitTypeRecord typeError(String err, Exception e) {
     if (opts.logErrors()) {
-      if (e == null) {
-        log.error(err);
-      } else {
-        log.error(err, e);
-      }
+      logger.atSevere().withCause(e).log(err);
       return defaultTypeError();
     }
     return SubmitTypeRecord.error(err);
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index ce236dc..e7ffd5e 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.index.Index;
@@ -41,12 +42,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Parses a query string meant to be applied to account objects. */
 public class AccountQueryBuilder extends QueryBuilder<AccountState> {
-  private static final Logger log = LoggerFactory.getLogger(AccountQueryBuilder.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String FIELD_ACCOUNT = "account";
   public static final String FIELD_CAN_SEE = "cansee";
@@ -226,7 +225,7 @@
     try {
       return canSeeSecondaryEmails();
     } catch (PermissionBackendException e) {
-      log.error("Permission check failed", e);
+      logger.atSevere().withCause(e).log("Permission check failed");
       return false;
     } catch (QueryParseException e) {
       // User is not signed in.
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 89f302b..346ac8e 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -30,11 +31,9 @@
 import com.google.inject.Provider;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
-  private static final Logger logger = LoggerFactory.getLogger(ChangeIsVisibleToPredicate.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected final Provider<ReviewDb> db;
   protected final ChangeNotes.Factory notesFactory;
@@ -74,7 +73,7 @@
     try {
       ProjectState projectState = projectCache.checkedGet(cd.project());
       if (projectState == null) {
-        logger.info("No such project: {}", cd.project());
+        logger.atInfo().log("No such project: %s", cd.project());
         return false;
       }
       if (!projectState.statePermitsRead()) {
@@ -94,8 +93,8 @@
     } catch (PermissionBackendException e) {
       Throwable cause = e.getCause();
       if (cause instanceof RepositoryNotFoundException) {
-        logger.warn(
-            "Skipping change {} because the corresponding repository was not found", cd.getId(), e);
+        logger.atWarning().withCause(e).log(
+            "Skipping change %s because the corresponding repository was not found", cd.getId());
         return false;
       }
       throw new OrmException("unable to check permissions on change " + cd.getId(), e);
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index e644579..3113504 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.errors.NotSignedInException;
@@ -772,18 +773,8 @@
       }
     }
 
-    // expand a group predicate into multiple user predicates
     if (group != null) {
-      Set<Account.Id> allMembers =
-          args.groupMembers.listAccounts(group).stream().map(Account::getId).collect(toSet());
-
-      int maxTerms = args.indexConfig.maxTerms();
-      if (allMembers.size() > maxTerms) {
-        // limit the number of query terms otherwise Gerrit will barf
-        accounts = ImmutableSet.copyOf(Iterables.limit(allMembers, maxTerms));
-      } else {
-        accounts = allMembers;
-      }
+      accounts = getMembers(group);
     }
 
     // If the vote piece looks like Code-Review=NEED with a valid non-numeric
@@ -972,12 +963,24 @@
   }
 
   @Operator
-  public Predicate<ChangeData> ownerin(String group) throws QueryParseException {
+  public Predicate<ChangeData> ownerin(String group) throws QueryParseException, IOException {
     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new OwnerinPredicate(args.userFactory, g.getUUID());
+
+    AccountGroup.UUID groupId = g.getUUID();
+    GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
+    if (!(groupDescription instanceof GroupDescription.Internal)) {
+      return new OwnerinPredicate(args.userFactory, groupId);
+    }
+
+    Set<Account.Id> accounts = getMembers(groupId);
+    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(accounts.size());
+    for (Account.Id id : accounts) {
+      p.add(new OwnerPredicate(id));
+    }
+    return Predicate.or(p);
   }
 
   @Operator
@@ -1248,6 +1251,20 @@
     return Predicate.and(predicates);
   }
 
+  private Set<Account.Id> getMembers(AccountGroup.UUID g) throws IOException {
+    Set<Account.Id> accounts;
+    Set<Account.Id> allMembers =
+        args.groupMembers.listAccounts(g).stream().map(Account::getId).collect(toSet());
+    int maxTerms = args.indexConfig.maxTerms();
+    if (allMembers.size() > maxTerms) {
+      // limit the number of query terms otherwise Gerrit will barf
+      accounts = ImmutableSet.copyOf(Iterables.limit(allMembers, maxTerms));
+    } else {
+      accounts = allMembers;
+    }
+    return accounts;
+  }
+
   private Set<Account.Id> parseAccount(String who)
       throws QueryParseException, OrmException, IOException, ConfigInvalidException {
     if (isSelf(who)) {
diff --git a/java/com/google/gerrit/server/query/change/ConflictKey.java b/java/com/google/gerrit/server/query/change/ConflictKey.java
index 0101ffe..9daf886 100644
--- a/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -14,62 +14,80 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.client.SubmitType;
-import java.io.Serializable;
-import java.util.Objects;
+import com.google.gerrit.server.cache.CacheSerializer;
+import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
-public class ConflictKey implements Serializable {
-  private static final long serialVersionUID = 2L;
-
-  private final ObjectId commit;
-  private final ObjectId otherCommit;
-  private final SubmitType submitType;
-  private final boolean contentMerge;
-
-  public ConflictKey(
-      ObjectId commit, ObjectId otherCommit, SubmitType submitType, boolean contentMerge) {
-    if (SubmitType.FAST_FORWARD_ONLY.equals(submitType) || commit.compareTo(otherCommit) < 0) {
-      this.commit = commit;
-      this.otherCommit = otherCommit;
-    } else {
-      this.commit = otherCommit;
-      this.otherCommit = commit;
+@AutoValue
+public abstract class ConflictKey {
+  public static ConflictKey create(
+      AnyObjectId commit, AnyObjectId otherCommit, SubmitType submitType, boolean contentMerge) {
+    ObjectId commitCopy = commit.copy();
+    ObjectId otherCommitCopy = otherCommit.copy();
+    if (submitType == SubmitType.FAST_FORWARD_ONLY) {
+      // The conflict check for FF-only is non-symmetrical, and we need to treat (X, Y) differently
+      // from (Y, X). Store the commits in the input order.
+      return new AutoValue_ConflictKey(commitCopy, otherCommitCopy, submitType, contentMerge);
     }
-    this.submitType = submitType;
-    this.contentMerge = contentMerge;
+    // Otherwise, the check is symmetrical; sort commit/otherCommit before storing, so the actual
+    // key is independent of the order in which they are passed to this method.
+    return new AutoValue_ConflictKey(
+        Ordering.natural().min(commitCopy, otherCommitCopy),
+        Ordering.natural().max(commitCopy, otherCommitCopy),
+        submitType,
+        contentMerge);
   }
 
-  public ObjectId getCommit() {
-    return commit;
+  @VisibleForTesting
+  static ConflictKey createWithoutNormalization(
+      AnyObjectId commit, AnyObjectId otherCommit, SubmitType submitType, boolean contentMerge) {
+    return new AutoValue_ConflictKey(commit.copy(), otherCommit.copy(), submitType, contentMerge);
   }
 
-  public ObjectId getOtherCommit() {
-    return otherCommit;
-  }
+  public abstract ObjectId commit();
 
-  public SubmitType getSubmitType() {
-    return submitType;
-  }
+  public abstract ObjectId otherCommit();
 
-  public boolean isContentMerge() {
-    return contentMerge;
-  }
+  public abstract SubmitType submitType();
 
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof ConflictKey)) {
-      return false;
+  public abstract boolean contentMerge();
+
+  public static enum Serializer implements CacheSerializer<ConflictKey> {
+    INSTANCE;
+
+    private static final Converter<String, SubmitType> SUBMIT_TYPE_CONVERTER =
+        Enums.stringConverter(SubmitType.class);
+
+    @Override
+    public byte[] serialize(ConflictKey object) {
+      ObjectIdConverter idConverter = ObjectIdConverter.create();
+      return ProtoCacheSerializers.toByteArray(
+          ConflictKeyProto.newBuilder()
+              .setCommit(idConverter.toByteString(object.commit()))
+              .setOtherCommit(idConverter.toByteString(object.otherCommit()))
+              .setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(object.submitType()))
+              .setContentMerge(object.contentMerge())
+              .build());
     }
-    ConflictKey other = (ConflictKey) o;
-    return commit.equals(other.commit)
-        && otherCommit.equals(other.otherCommit)
-        && submitType.equals(other.submitType)
-        && contentMerge == other.contentMerge;
-  }
 
-  @Override
-  public int hashCode() {
-    return Objects.hash(commit, otherCommit, submitType, contentMerge);
+    @Override
+    public ConflictKey deserialize(byte[] in) {
+      ConflictKeyProto proto = ProtoCacheSerializers.parseUnchecked(ConflictKeyProto.parser(), in);
+      ObjectIdConverter idConverter = ObjectIdConverter.create();
+      return create(
+          idConverter.fromByteString(proto.getCommit()),
+          idConverter.fromByteString(proto.getOtherCommit()),
+          SUBMIT_TYPE_CONVERTER.convert(proto.getSubmitType()),
+          proto.getContentMerge());
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/ConflictsCache.java b/java/com/google/gerrit/server/query/change/ConflictsCache.java
index e8b2fef..c7ee79b 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsCache.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsCache.java
@@ -18,7 +18,7 @@
 
 public interface ConflictsCache {
 
-  void put(ConflictKey key, Boolean value);
+  void put(ConflictKey key, boolean value);
 
   @Nullable
   Boolean getIfPresent(ConflictKey key);
diff --git a/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java b/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
index 1185677..0b8c5ee 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.cache.Cache;
+import com.google.gerrit.server.cache.BooleanCacheSerializer;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -29,7 +30,11 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        persist(NAME, ConflictKey.class, Boolean.class).maximumWeight(37400);
+        persist(NAME, ConflictKey.class, Boolean.class)
+            .version(1)
+            .keySerializer(ConflictKey.Serializer.INSTANCE)
+            .valueSerializer(BooleanCacheSerializer.INSTANCE)
+            .maximumWeight(37400);
         bind(ConflictsCache.class).to(ConflictsCacheImpl.class);
       }
     };
@@ -43,7 +48,7 @@
   }
 
   @Override
-  public void put(ConflictKey key, Boolean value) {
+  public void put(ConflictKey key, boolean value) {
     conflictsCache.put(key, value);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index f870951..7dc7a0b 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -115,19 +115,19 @@
 
       ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
       ConflictKey conflictsKey =
-          new ConflictKey(
+          ConflictKey.create(
               changeDataCache.getTestAgainst(),
               other,
               str.type,
               projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
-      Boolean conflicts = args.conflictsCache.getIfPresent(conflictsKey);
-      if (conflicts != null) {
-        return conflicts;
+      Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
+      if (maybeConflicts != null) {
+        return maybeConflicts;
       }
 
       try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
           CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-        conflicts =
+        boolean conflicts =
             !args.submitDryRun.run(
                 str.type,
                 repo,
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 0806edf..dc57a9b 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.index.query.QueryParseException;
@@ -54,8 +55,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Change query implementation that outputs to a stream in the style of an SSH command.
@@ -64,7 +63,7 @@
  * holding on to a single instance.
  */
 public class OutputStreamQuery {
-  private static final Logger log = LoggerFactory.getLogger(OutputStreamQuery.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final DateTimeFormatter dtf =
       DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz")
@@ -214,7 +213,7 @@
         stats.runTimeMilliseconds = TimeUtil.nowMs() - stats.runTimeMilliseconds;
         show(stats);
       } catch (OrmException err) {
-        log.error("Cannot execute query: " + queryString, err);
+        logger.atSevere().withCause(err).log("Cannot execute query: %s", queryString);
 
         ErrorMessage m = new ErrorMessage();
         m.message = "cannot query database";
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index a9de2b1..17d6448 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
@@ -25,11 +26,9 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ParentProjectPredicate extends OrPredicate<ChangeData> {
-  private static final Logger log = LoggerFactory.getLogger(ParentProjectPredicate.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected final String value;
 
@@ -53,7 +52,7 @@
         r.add(new ProjectPredicate(p.name));
       }
     } catch (PermissionBackendException e) {
-      log.warn("cannot check permissions to expand child projects", e);
+      logger.atWarning().withCause(e).log("cannot check permissions to expand child projects");
     }
     return r;
   }
diff --git a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index a3566a5..4f751c5 100644
--- a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class TrackingIdPredicate extends ChangeIndexPredicate {
-  private static final Logger log = LoggerFactory.getLogger(TrackingIdPredicate.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public TrackingIdPredicate(String trackingId) {
     super(ChangeField.TR, trackingId);
@@ -32,7 +31,7 @@
     try {
       return cd.trackingFooters().containsValue(getValue());
     } catch (IOException e) {
-      log.warn("Cannot extract footers from " + cd.getId(), e);
+      logger.atWarning().withCause(e).log("Cannot extract footers from %s", cd.getId());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
index 7a3a905..d9808f2 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.index.query.Predicate;
@@ -29,8 +30,6 @@
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Query wrapper for the group index.
@@ -39,7 +38,7 @@
  * holding on to a single instance.
  */
 public class InternalGroupQuery extends InternalQuery<InternalGroup> {
-  private static final Logger log = LoggerFactory.getLogger(InternalGroupQuery.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject
   InternalGroupQuery(
@@ -76,7 +75,7 @@
 
     ImmutableList<AccountGroup.UUID> groupUuids =
         groups.stream().map(InternalGroup::getGroupUUID).collect(toImmutableList());
-    log.warn(String.format("Ambiguous %s for groups %s.", groupDescription, groupUuids));
+    logger.atWarning().log("Ambiguous %s for groups %s.", groupDescription, groupUuids);
     return Optional.empty();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 6c2a50d..ff114fae 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -29,9 +29,9 @@
         "//lib/commons:codec",
         "//lib/commons:compress",
         "//lib/commons:lang",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
     ],
 )
diff --git a/java/com/google/gerrit/server/restapi/RestApiModule.java b/java/com/google/gerrit/server/restapi/RestApiModule.java
index 1ba6f22..dc2fe5f 100644
--- a/java/com/google/gerrit/server/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/RestApiModule.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi;
 
+import com.google.gerrit.server.plugins.PluginRestApiModule;
+import com.google.gerrit.server.restapi.config.RestCacheAdminModule;
 import com.google.inject.AbstractModule;
 
 public class RestApiModule extends AbstractModule {
@@ -23,7 +25,9 @@
     install(new com.google.gerrit.server.restapi.account.Module());
     install(new com.google.gerrit.server.restapi.change.Module());
     install(new com.google.gerrit.server.restapi.config.Module());
+    install(new RestCacheAdminModule());
     install(new com.google.gerrit.server.restapi.group.Module());
+    install(new PluginRestApiModule());
     install(new com.google.gerrit.server.restapi.project.Module());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/access/AccessCollection.java b/java/com/google/gerrit/server/restapi/access/AccessCollection.java
index d4528c5..8ae2ce7 100644
--- a/java/com/google/gerrit/server/restapi/access/AccessCollection.java
+++ b/java/com/google/gerrit/server/restapi/access/AccessCollection.java
@@ -30,7 +30,7 @@
   private final DynamicMap<RestView<AccessResource>> views;
 
   @Inject
-  AccessCollection(Provider<ListAccess> list, DynamicMap<RestView<AccessResource>> views) {
+  public AccessCollection(Provider<ListAccess> list, DynamicMap<RestView<AccessResource>> views) {
     this.list = list;
     this.views = views;
   }
diff --git a/java/com/google/gerrit/server/restapi/access/ListAccess.java b/java/com/google/gerrit/server/restapi/access/ListAccess.java
index a79afd2..3f01c6c 100644
--- a/java/com/google/gerrit/server/restapi/access/ListAccess.java
+++ b/java/com/google/gerrit/server/restapi/access/ListAccess.java
@@ -34,11 +34,10 @@
 public class ListAccess implements RestReadView<TopLevelResource> {
 
   @Option(
-    name = "--project",
-    aliases = {"-p"},
-    metaVar = "PROJECT",
-    usage = "projects for which the access rights should be returned"
-  )
+      name = "--project",
+      aliases = {"-p"},
+      metaVar = "PROJECT",
+      usage = "projects for which the access rights should be returned")
   private List<String> projects = new ArrayList<>();
 
   private final GetAccess getAccess;
diff --git a/java/com/google/gerrit/server/restapi/account/AddSshKey.java b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
index 7539cf3..ab06e25 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteSource;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -43,12 +44,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AddSshKey implements RestModifyView<AccountResource, SshKeyInput> {
-  private static final Logger log = LoggerFactory.getLogger(AddSshKey.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
@@ -74,7 +73,7 @@
   public Response<SshKeyInfo> apply(AccountResource rsrc, SshKeyInput input)
       throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
     return apply(rsrc.getUser(), input);
@@ -104,8 +103,8 @@
       try {
         addKeyFactory.create(user, sshKey).send();
       } catch (EmailException e) {
-        log.error(
-            "Cannot send SSH key added message to " + user.getAccount().getPreferredEmail(), e);
+        logger.atSevere().withCause(e).log(
+            "Cannot send SSH key added message to %s", user.getAccount().getPreferredEmail());
       }
 
       user.getUserName().ifPresent(sshKeyCache::evict);
diff --git a/java/com/google/gerrit/server/restapi/account/Capabilities.java b/java/com/google/gerrit/server/restapi/account/Capabilities.java
index 8047b26..ec16e2b 100644
--- a/java/com/google/gerrit/server/restapi/account/Capabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/Capabilities.java
@@ -38,7 +38,7 @@
 import java.util.Optional;
 
 @Singleton
-class Capabilities implements ChildCollection<AccountResource, AccountResource.Capability> {
+public class Capabilities implements ChildCollection<AccountResource, AccountResource.Capability> {
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
   private final DynamicMap<RestView<AccountResource.Capability>> views;
@@ -66,7 +66,7 @@
       throws ResourceNotFoundException, AuthException, PermissionBackendException {
     permissionBackend.checkUsesDefaultCapabilities();
     IdentifiedUser target = parent.getUser();
-    if (self.get() != target) {
+    if (!self.get().hasSameAccountId(target)) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 57f7d4a..abc6dd9 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.extensions.client.AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
@@ -45,11 +46,9 @@
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class CreateEmail implements RestModifyView<AccountResource, EmailInput> {
-  private static final Logger log = LoggerFactory.getLogger(CreateEmail.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     CreateEmail create(String email);
@@ -95,7 +94,7 @@
       input = new EmailInput();
     }
 
-    if (self.get() != rsrc.getUser() || input.noConfirmation) {
+    if (!self.get().hasSameAccountId(rsrc.getUser()) || input.noConfirmation) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
@@ -126,7 +125,7 @@
     info.email = email;
     if (input.noConfirmation || isDevMode) {
       if (isDevMode) {
-        log.warn("skipping email validation in developer mode");
+        logger.atWarning().log("skipping email validation in developer mode");
       }
       try {
         accountManager.link(user.getAccountId(), AuthRequest.forEmail(email));
@@ -146,7 +145,7 @@
         sender.send();
         info.pendingConfirmation = true;
       } catch (EmailException | RuntimeException e) {
-        log.error("Cannot send email verification message to " + email, e);
+        logger.atSevere().withCause(e).log("Cannot send email verification message to %s", email);
         throw e;
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteActive.java b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
index fda28c9..4302513 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteActive.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteActive.java
@@ -47,7 +47,7 @@
   @Override
   public Response<?> apply(AccountResource rsrc, Input input)
       throws RestApiException, OrmException, IOException, ConfigInvalidException {
-    if (self.get() == rsrc.getUser()) {
+    if (self.get().hasSameAccountId(rsrc.getUser())) {
       throw new ResourceConflictException("cannot deactivate own account");
     }
     return setInactiveFlag.deactivate(rsrc.getUser().getAccountId());
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
index d556810..f0269f1 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
@@ -71,7 +71,7 @@
       throws AuthException, ResourceNotFoundException, ResourceConflictException,
           MethodNotAllowedException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index c40ed29..05b1771 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -68,7 +68,7 @@
   public Response<?> apply(AccountResource resource, List<String> extIds)
       throws RestApiException, IOException, OrmException, ConfigInvalidException,
           PermissionBackendException {
-    if (self.get() != resource.getUser()) {
+    if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index 998c6f0..b7b3c83 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -57,7 +57,7 @@
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
       throws AuthException, OrmException, RepositoryNotFoundException, IOException,
           ConfigInvalidException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index ce10b38..0e2edb9 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -61,7 +61,7 @@
   public Response<?> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
       throws AuthException, UnprocessableEntityException, OrmException, IOException,
           ConfigInvalidException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
     if (input == null) {
diff --git a/java/com/google/gerrit/server/restapi/account/EmailsCollection.java b/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
index c7a6bae..8694da0 100644
--- a/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
@@ -64,7 +64,7 @@
   @Override
   public AccountResource.Email parse(AccountResource rsrc, IdString id)
       throws ResourceNotFoundException, PermissionBackendException, AuthException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetAgreements.java b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
index 719cb21..dced4d7 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAgreements.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
@@ -36,12 +37,10 @@
 import java.util.Collection;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class GetAgreements implements RestReadView<AccountResource> {
-  private static final Logger log = LoggerFactory.getLogger(GetAgreements.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<CurrentUser> self;
   private final ProjectCache projectCache;
@@ -85,13 +84,9 @@
           if (rule.getGroup().getUUID() != null) {
             groupIds.add(rule.getGroup().getUUID());
           } else {
-            log.warn(
-                "group \""
-                    + rule.getGroup().getName()
-                    + "\" does not "
-                    + "exist, referenced in CLA \""
-                    + ca.getName()
-                    + "\"");
+            logger.atWarning().log(
+                "group \"%s\" does not exist, referenced in CLA \"%s\"",
+                rule.getGroup().getName(), ca.getName());
           }
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/account/GetAvatar.java b/java/com/google/gerrit/server/restapi/account/GetAvatar.java
index 2f8570e..3c1752d 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAvatar.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAvatar.java
@@ -32,10 +32,9 @@
   private int size;
 
   @Option(
-    name = "--size",
-    aliases = {"-s"},
-    usage = "recommended size in pixels, height and width"
-  )
+      name = "--size",
+      aliases = {"-s"},
+      usage = "recommended size in pixels, height and width")
   public void setSize(int s) {
     size = s;
   }
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index f38d367..d2236fd 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -50,7 +50,7 @@
 import java.util.Set;
 import org.kohsuke.args4j.Option;
 
-class GetCapabilities implements RestReadView<AccountResource> {
+public class GetCapabilities implements RestReadView<AccountResource> {
   @Option(name = "-q", metaVar = "CAP", usage = "Capability to inspect")
   void addQuery(String name) {
     if (query == null) {
@@ -79,12 +79,13 @@
   }
 
   @Override
-  public Object apply(AccountResource rsrc) throws RestApiException, PermissionBackendException {
+  public Object apply(AccountResource resource)
+      throws RestApiException, PermissionBackendException {
     permissionBackend.checkUsesDefaultCapabilities();
     PermissionBackend.WithUser perm = permissionBackend.currentUser();
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(resource.getUser())) {
       perm.check(GlobalPermission.ADMINISTRATE_SERVER);
-      perm = permissionBackend.user(rsrc.getUser());
+      perm = permissionBackend.user(resource.getUser());
     }
 
     Map<String, Object> have = new LinkedHashMap<>();
@@ -92,7 +93,7 @@
       have.put(globalOrPluginPermissionName(p), true);
     }
 
-    AccountLimits limits = limitsFactory.create(rsrc.getUser());
+    AccountLimits limits = limitsFactory.create(resource.getUser());
     addRanges(have, limits);
     addPriority(have, limits);
 
@@ -162,7 +163,7 @@
   }
 
   @Singleton
-  static class CheckOne implements RestReadView<AccountResource.Capability> {
+  public static class CheckOne implements RestReadView<AccountResource.Capability> {
     private final PermissionBackend permissionBackend;
 
     @Inject
diff --git a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
index f24991d..0ecd6ea 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
@@ -50,7 +50,7 @@
   @Override
   public EditPreferencesInfo apply(AccountResource rsrc)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index 2f72ad7..7a420ab 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -61,7 +61,7 @@
   @Override
   public List<AccountExternalIdInfo> apply(AccountResource resource)
       throws RestApiException, IOException, OrmException, PermissionBackendException {
-    if (self.get() != resource.getUser()) {
+    if (!self.get().hasSameAccountId(resource.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java b/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
index fca1669..395c159 100644
--- a/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
+++ b/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -27,14 +28,12 @@
 import com.google.inject.Singleton;
 import java.net.URI;
 import java.net.URISyntaxException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
-class GetOAuthToken implements RestReadView<AccountResource> {
+public class GetOAuthToken implements RestReadView<AccountResource> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String BEARER_TYPE = "bearer";
-  private static final Logger log = LoggerFactory.getLogger(GetOAuthToken.class);
 
   private final Provider<CurrentUser> self;
   private final OAuthTokenCache tokenCache;
@@ -53,7 +52,7 @@
   @Override
   public OAuthTokenInfo apply(AccountResource rsrc)
       throws AuthException, ResourceNotFoundException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       throw new AuthException("not allowed to get access token");
     }
     OAuthToken accessToken = tokenCache.get(rsrc.getUser().getAccountId());
@@ -72,14 +71,15 @@
 
   private static String getHostName(String canonicalWebUrl) {
     if (canonicalWebUrl == null) {
-      log.error("No canonicalWebUrl defined in gerrit.config, OAuth may not work properly");
+      logger.atSevere().log(
+          "No canonicalWebUrl defined in gerrit.config, OAuth may not work properly");
       return null;
     }
 
     try {
       return new URI(canonicalWebUrl).getHost();
     } catch (URISyntaxException e) {
-      log.error("Invalid canonicalWebUrl '" + canonicalWebUrl + "'", e);
+      logger.atSevere().withCause(e).log("Invalid canonicalWebUrl '%s'", canonicalWebUrl);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/account/GetPreferences.java b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
index e32d434..a185898 100644
--- a/java/com/google/gerrit/server/restapi/account/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
@@ -48,7 +48,7 @@
   @Override
   public GeneralPreferencesInfo apply(AccountResource rsrc)
       throws RestApiException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index 15ad75f..a49f9df 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -57,7 +57,7 @@
   public List<SshKeyInfo> apply(AccountResource rsrc)
       throws AuthException, OrmException, RepositoryNotFoundException, IOException,
           ConfigInvalidException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser());
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 3a6595c..112bb24 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -61,7 +61,7 @@
   public List<ProjectWatchInfo> apply(AccountResource rsrc)
       throws OrmException, AuthException, IOException, ConfigInvalidException,
           PermissionBackendException, ResourceNotFoundException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/Index.java b/java/com/google/gerrit/server/restapi/account/Index.java
index 0d8171d..6ddfc0f4 100644
--- a/java/com/google/gerrit/server/restapi/account/Index.java
+++ b/java/com/google/gerrit/server/restapi/account/Index.java
@@ -49,7 +49,7 @@
   @Override
   public Response<?> apply(AccountResource rsrc, Input input)
       throws IOException, AuthException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index 6486d18..f29a0eb 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -68,7 +68,7 @@
   public List<ProjectWatchInfo> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
       throws OrmException, RestApiException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutAgreement.java b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
index d46d2c0..3f1a833 100644
--- a/java/com/google/gerrit/server/restapi/account/PutAgreement.java
+++ b/java/com/google/gerrit/server/restapi/account/PutAgreement.java
@@ -72,7 +72,7 @@
       throw new MethodNotAllowedException("contributor agreements disabled");
     }
 
-    if (self.get() != resource.getUser()) {
+    if (!self.get().hasSameAccountId(resource.getUser())) {
       throw new AuthException("not allowed to enter contributor agreement");
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 45f4ca8..e42e5d1 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -76,7 +76,7 @@
   public Response<String> apply(AccountResource rsrc, HttpPasswordInput input)
       throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
           IOException, ConfigInvalidException, PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index a982331..1e00aac 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -62,7 +62,7 @@
   public Response<String> apply(AccountResource rsrc, NameInput input)
       throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
           IOException, PermissionBackendException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), input);
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index 65285c3..51d28ed 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -44,12 +45,10 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class PutPreferred implements RestModifyView<AccountResource.Email, Input> {
-  private static final Logger log = LoggerFactory.getLogger(PutPreferred.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
@@ -117,9 +116,9 @@
                         externalIds.byEmail(preferredEmail);
                     if (!existingExtIdsWithThisEmail.isEmpty()) {
                       // but the email is already assigned to another account
-                      log.warn(
-                          "Cannot set preferred email {} for account {} because it is owned"
-                              + " by the following account(s): {}",
+                      logger.atWarning().log(
+                          "Cannot set preferred email %s for account %s because it is owned"
+                              + " by the following account(s): %s",
                           preferredEmail,
                           user.getAccountId(),
                           existingExtIdsWithThisEmail
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index 3c173a0..9aee0a3 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -56,7 +56,7 @@
   public Response<String> apply(AccountResource rsrc, StatusInput input)
       throws AuthException, ResourceNotFoundException, OrmException, IOException,
           PermissionBackendException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), input);
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 073d724..856a5db 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -75,7 +75,7 @@
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
           ResourceConflictException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 516b485..8784d23 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -73,11 +73,10 @@
   }
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of users to return"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of users to return")
   public void setLimit(int n) {
     queryProcessor.setUserProvidedLimit(n);
 
@@ -101,21 +100,19 @@
   }
 
   @Option(
-    name = "--query",
-    aliases = {"-q"},
-    metaVar = "QUERY",
-    usage = "match users"
-  )
+      name = "--query",
+      aliases = {"-q"},
+      metaVar = "QUERY",
+      usage = "match users")
   public void setQuery(String query) {
     this.query = query;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "Number of accounts to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "Number of accounts to skip")
   public void setStart(int start) {
     this.start = start;
   }
diff --git a/java/com/google/gerrit/server/restapi/account/SshKeys.java b/java/com/google/gerrit/server/restapi/account/SshKeys.java
index fa447c2..4e44c71 100644
--- a/java/com/google/gerrit/server/restapi/account/SshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/SshKeys.java
@@ -66,7 +66,7 @@
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
       throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    if (self.get() != rsrc.getUser()) {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
       try {
         permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
       } catch (AuthException e) {
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 1d3fae0..e804b64 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -45,14 +46,12 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class StarredChanges
     implements ChildCollection<AccountResource, AccountResource.StarredChange>,
         AcceptsCreate<AccountResource> {
-  private static final Logger log = LoggerFactory.getLogger(StarredChanges.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ChangesCollection changes;
   private final DynamicMap<RestView<AccountResource.StarredChange>> views;
@@ -110,7 +109,7 @@
     } catch (ResourceNotFoundException e) {
       throw new UnprocessableEntityException(String.format("change %s not found", id.get()));
     } catch (OrmException | PermissionBackendException | IOException e) {
-      log.error("cannot resolve change", e);
+      logger.atSevere().withCause(e).log("cannot resolve change");
       throw new UnprocessableEntityException("internal server error");
     }
   }
@@ -135,7 +134,7 @@
     @Override
     public Response<?> apply(AccountResource rsrc, EmptyInput in)
         throws RestApiException, OrmException, IOException {
-      if (self.get() != rsrc.getUser()) {
+      if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to add starred change");
       }
       try {
@@ -157,7 +156,7 @@
   }
 
   @Singleton
-  static class Put implements RestModifyView<AccountResource.StarredChange, EmptyInput> {
+  public static class Put implements RestModifyView<AccountResource.StarredChange, EmptyInput> {
     private final Provider<CurrentUser> self;
 
     @Inject
@@ -168,7 +167,7 @@
     @Override
     public Response<?> apply(AccountResource.StarredChange rsrc, EmptyInput in)
         throws AuthException {
-      if (self.get() != rsrc.getUser()) {
+      if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed update starred changes");
       }
       return Response.none();
@@ -189,7 +188,7 @@
     @Override
     public Response<?> apply(AccountResource.StarredChange rsrc, EmptyInput in)
         throws AuthException, OrmException, IOException, IllegalLabelException {
-      if (self.get() != rsrc.getUser()) {
+      if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed remove starred change");
       }
       starredChangesUtil.star(
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
index 6862e8a..fb809ee 100644
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ b/java/com/google/gerrit/server/restapi/account/Stars.java
@@ -100,7 +100,7 @@
     @SuppressWarnings("unchecked")
     public List<ChangeInfo> apply(AccountResource rsrc)
         throws BadRequestException, AuthException, OrmException {
-      if (self.get() != rsrc.getUser()) {
+      if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to list stars of another account");
       }
       QueryChanges query = changes.list();
@@ -122,7 +122,7 @@
 
     @Override
     public SortedSet<String> apply(AccountResource.Star rsrc) throws AuthException, OrmException {
-      if (self.get() != rsrc.getUser()) {
+      if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to get stars of another account");
       }
       return starredChangesUtil.getLabels(self.get().getAccountId(), rsrc.getChange().getId());
@@ -143,7 +143,7 @@
     @Override
     public Collection<String> apply(AccountResource.Star rsrc, StarsInput in)
         throws AuthException, BadRequestException, OrmException {
-      if (self.get() != rsrc.getUser()) {
+      if (!self.get().hasSameAccountId(rsrc.getUser())) {
         throw new AuthException("not allowed to update stars of another account");
       }
       try {
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 116cbdb..7978990 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -46,13 +47,11 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo>
     implements UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Abandon.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
@@ -160,10 +159,8 @@
         return description;
       }
     } catch (OrmException | IOException e) {
-      log.error(
-          String.format(
-              "Failed to check if the current patch set of change %s is locked", change.getId()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index fe8d8bd..b7a029b 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -383,10 +383,9 @@
     private final ProjectCache projectCache;
 
     @Option(
-      name = "--base",
-      aliases = {"-b"},
-      usage = "whether to load the content on the base revision instead of the change edit"
-    )
+        name = "--base",
+        aliases = {"-b"},
+        usage = "whether to load the content on the base revision instead of the change edit")
     private boolean base;
 
     @Inject
@@ -485,10 +484,9 @@
     private final ChangeEditUtil editUtil;
 
     @Option(
-      name = "--base",
-      aliases = {"-b"},
-      usage = "whether to load the message on the base revision instead of the change edit"
-    )
+        name = "--base",
+        aliases = {"-b"},
+        usage = "whether to load the message on the base revision instead of the change edit")
     private boolean base;
 
     @Inject
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index 3609b5f..53d81d7 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -46,14 +47,13 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class CherryPick
     extends RetryingRestModifyView<RevisionResource, CherryPickInput, ChangeInfo>
     implements UiAction<RevisionResource> {
-  private static final Logger log = LoggerFactory.getLogger(CherryPick.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final PermissionBackend permissionBackend;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
@@ -120,7 +120,8 @@
     try {
       projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
     } catch (IOException e) {
-      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if project state permits write: %s", rsrc.getProject());
     }
     return new UiAction.Description()
         .setLabel("Cherry Pick")
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 9f86e46..73669a1 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -171,7 +171,8 @@
       throw new BadRequestException("branch must be non-empty");
     }
 
-    if (Strings.isNullOrEmpty(input.subject)) {
+    String subject = clean(Strings.nullToEmpty(input.subject));
+    if (Strings.isNullOrEmpty(subject)) {
       throw new BadRequestException("commit message must be non-empty");
     }
 
@@ -264,7 +265,7 @@
       GeneralPreferencesInfo info = accountState.getGeneralPreferences();
 
       // Add a Change-Id line if there isn't already one
-      String commitMessage = input.subject;
+      String commitMessage = subject;
       if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
         ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
         ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, commitMessage);
@@ -388,4 +389,16 @@
   private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
     return inserter.insert(new TreeFormatter());
   }
+
+  /**
+   * Remove comment lines from a commit message.
+   *
+   * <p>Based on {@link org.eclipse.jgit.util.ChangeIdUtil#clean}.
+   *
+   * @param msg
+   * @return message without comment lines, possibly empty.
+   */
+  private String clean(String msg) {
+    return msg.replaceAll("(?m)^#.*$\n?", "").trim();
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
index f06709d..fac1003 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.reviewdb.client.Change;
@@ -30,11 +31,9 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.Collections;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class DeleteReviewerByEmailOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(DeleteReviewerByEmailOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
@@ -100,7 +99,7 @@
       cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
       cm.send();
     } catch (Exception err) {
-      log.error("Cannot email update for change " + change.getId(), err);
+      logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
index 3938034..91f5d15 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
@@ -57,11 +58,9 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class DeleteReviewerOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(DeleteReviewerOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     DeleteReviewerOp create(AccountState reviewerAccount, DeleteReviewerInput input);
@@ -248,7 +247,7 @@
       cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
       cm.send();
     } catch (Exception err) {
-      log.error("Cannot email update for change " + change.getId(), err);
+      logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index b8229ac..f268a30 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
@@ -64,12 +65,10 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
-  private static final Logger log = LoggerFactory.getLogger(DeleteVote.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<ReviewDb> db;
   private final ApprovalsUtil approvalsUtil;
@@ -252,7 +251,7 @@
           cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
           cm.send();
         } catch (Exception e) {
-          log.error("Cannot email update for change " + change.getId(), e);
+          logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
         }
       }
 
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index e391f9b..bb2f668 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -70,8 +71,6 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Files implements ChildCollection<RevisionResource, FileResource> {
@@ -100,7 +99,7 @@
   }
 
   public static final class ListFiles implements ETagView<RevisionResource> {
-    private static final Logger log = LoggerFactory.getLogger(ListFiles.class);
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
     @Option(name = "--base", metaVar = "revision-id")
     String base;
@@ -248,9 +247,9 @@
         try {
           return copy(res.files(), res.patchSetId(), resource, userId);
         } catch (PatchListObjectTooLargeException e) {
-          log.warn("Cannot copy patch review flags: " + e.getMessage());
+          logger.atWarning().log("Cannot copy patch review flags: %s", e.getMessage());
         } catch (IOException | PatchListNotAvailableException e) {
-          log.warn("Cannot copy patch review flags", e);
+          logger.atWarning().withCause(e).log("Cannot copy patch review flags");
         }
       }
 
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index f5c8849..c7a8015 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -58,12 +58,11 @@
   private final AutoMerger autoMerger;
 
   @Option(
-    name = "--base",
-    aliases = {"-b"},
-    usage =
-        "whether to load the blame of the base revision (the direct"
-            + " parent of the change) instead of the change"
-  )
+      name = "--base",
+      aliases = {"-b"},
+      usage =
+          "whether to load the blame of the base revision (the direct"
+              + " parent of the change) instead of the change")
   private boolean base;
 
   @Inject
diff --git a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
index 4b26c5c..42675f6 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
@@ -32,10 +32,9 @@
   private final PureRevert pureRevert;
 
   @Option(
-    name = "--claimed-original",
-    aliases = {"-o"},
-    usage = "SHA1 (40 digit hex) of the original commit"
-  )
+      name = "--claimed-original",
+      aliases = {"-o"},
+      usage = "SHA1 (40 digit hex) of the original commit")
   @Nullable
   private String claimedOriginal;
 
diff --git a/java/com/google/gerrit/server/restapi/change/Ignore.java b/java/com/google/gerrit/server/restapi/change/Ignore.java
index d710539..e319451 100644
--- a/java/com/google/gerrit/server/restapi/change/Ignore.java
+++ b/java/com/google/gerrit/server/restapi/change/Ignore.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -28,12 +29,10 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Ignore implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Ignore.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final StarredChangesUtil stars;
 
@@ -75,7 +74,7 @@
     try {
       return stars.isIgnored(rsrc);
     } catch (OrmException e) {
-      log.error("failed to check ignored star", e);
+      logger.atSevere().withCause(e).log("failed to check ignored star");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
index af64a92..7c9ba73 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -28,13 +29,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class MarkAsReviewed
     implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(MarkAsReviewed.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
@@ -71,7 +70,7 @@
           .create(dbProvider.get(), rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
     } catch (OrmException e) {
-      log.error("failed to check if change is reviewed", e);
+      logger.atSevere().withCause(e).log("failed to check if change is reviewed");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
index 2e74d61..6e15dcc 100644
--- a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
+++ b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -27,13 +28,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class MarkAsUnreviewed
     implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(MarkAsUnreviewed.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
@@ -70,7 +69,7 @@
           .create(dbProvider.get(), rsrc.getNotes())
           .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
     } catch (OrmException e) {
-      log.error("failed to check if change is reviewed", e);
+      logger.atSevere().withCause(e).log("failed to check if change is reviewed");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index 4f8064b..b196347 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.MergeableInfo;
@@ -51,17 +52,14 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class Mergeable implements RestReadView<RevisionResource> {
-  private static final Logger log = LoggerFactory.getLogger(Mergeable.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Option(
-    name = "--other-branches",
-    aliases = {"-o"},
-    usage = "test mergeability for other branches too"
-  )
+      name = "--other-branches",
+      aliases = {"-o"},
+      usage = "test mergeability for other branches too")
   private boolean otherBranches;
 
   private final GitRepositoryManager gitManager;
@@ -176,7 +174,7 @@
     try {
       return ObjectId.fromString(ps.getRevision().get());
     } catch (IllegalArgumentException e) {
-      log.error("Invalid revision on patch set " + ps);
+      logger.atSevere().log("Invalid revision on patch set %s", ps);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 6e049438..5fcf967 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.changes.MoveInput;
@@ -70,13 +71,11 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo>
     implements UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Move.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
@@ -298,7 +297,8 @@
         return description;
       }
     } catch (IOException e) {
-      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if project state permits write: %s", rsrc.getProject());
       return description;
     }
 
@@ -307,10 +307,8 @@
         return description;
       }
     } catch (OrmException | IOException e) {
-      log.error(
-          String.format(
-              "Failed to check if the current patch set of change %s is locked", change.getId()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 579f965..489eaeb 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -85,6 +85,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.PublishCommentUtil;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.EmailReviewComments;
@@ -161,6 +162,7 @@
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
   private final CommentsUtil commentsUtil;
+  private final PublishCommentUtil publishCommentUtil;
   private final PatchSetUtil psUtil;
   private final PatchListCache patchListCache;
   private final AccountsCollection accounts;
@@ -183,6 +185,7 @@
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
       CommentsUtil commentsUtil,
+      PublishCommentUtil publishCommentUtil,
       PatchSetUtil psUtil,
       PatchListCache patchListCache,
       AccountsCollection accounts,
@@ -199,6 +202,7 @@
     this.changeResourceFactory = changeResourceFactory;
     this.changeDataFactory = changeDataFactory;
     this.commentsUtil = commentsUtil;
+    this.publishCommentUtil = publishCommentUtil;
     this.psUtil = psUtil;
     this.patchListCache = patchListCache;
     this.approvalsUtil = approvalsUtil;
@@ -948,7 +952,7 @@
       switch (in.drafts) {
         case PUBLISH:
         case PUBLISH_ALL_REVISIONS:
-          commentsUtil.publish(ctx, psId, drafts.values(), in.tag);
+          publishCommentUtil.publish(ctx, psId, drafts.values(), in.tag);
           comments.addAll(drafts.values());
           break;
         case KEEP:
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
index a406cc1..0502e91 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -58,11 +59,9 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class PostReviewersOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(PostReviewersOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     PostReviewersOp create(
@@ -262,7 +261,8 @@
       cm.addExtraCCByEmail(copiedByEmail);
       cm.send();
     } catch (Exception err) {
-      log.error("Cannot send email to new reviewers of change " + change.getId(), err);
+      logger.atSevere().withCause(err).log(
+          "Cannot send email to new reviewers of change %s", change.getId());
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 7beef20..1c9e420 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,11 +38,9 @@
 import java.util.EnumSet;
 import java.util.List;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class QueryChanges implements RestReadView<TopLevelResource> {
-  private static final Logger log = LoggerFactory.getLogger(QueryChanges.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ChangeJson.Factory json;
   private final ChangeQueryBuilder qb;
@@ -49,19 +48,17 @@
   private EnumSet<ListChangesOption> options;
 
   @Option(
-    name = "--query",
-    aliases = {"-q"},
-    metaVar = "QUERY",
-    usage = "Query string"
-  )
+      name = "--query",
+      aliases = {"-q"},
+      metaVar = "QUERY",
+      usage = "Query string")
   private List<String> queries;
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "Maximum number of results to return"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "Maximum number of results to return")
   public void setLimit(int limit) {
     imp.setUserProvidedLimit(limit);
   }
@@ -77,11 +74,10 @@
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "Number of changes to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "Number of changes to skip")
   public void setStart(int start) {
     imp.setStart(start);
   }
@@ -115,7 +111,7 @@
     } catch (QueryRequiresAuthException e) {
       throw new AuthException("Must be signed-in to use this operator");
     } catch (QueryParseException e) {
-      log.debug("Reject change query with 400 Bad Request: " + queries, e);
+      logger.atFine().withCause(e).log("Reject change query with 400 Bad Request: %s", queries);
       throw new BadRequestException(e.getMessage(), e);
     }
     return out.size() == 1 ? out.get(0) : out;
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 02e7c18..99a755ae 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -60,13 +61,12 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Rebase extends RetryingRestModifyView<RevisionResource, RebaseInput, ChangeInfo>
     implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
-  private static final Logger log = LoggerFactory.getLogger(Rebase.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final ImmutableSet<ListChangesOption> OPTIONS =
       Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
 
@@ -222,7 +222,8 @@
         return description;
       }
     } catch (IOException e) {
-      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if project state permits write: %s", rsrc.getProject());
       return description;
     }
 
@@ -231,10 +232,8 @@
         return description;
       }
     } catch (OrmException | IOException e) {
-      log.error(
-          String.format(
-              "Failed to check if the current patch set of change %s is locked", change.getId()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
     }
 
@@ -245,7 +244,8 @@
         enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
       }
     } catch (IOException e) {
-      log.error("Failed to check if patch set can be rebased: " + rsrc.getPatchSet(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if patch set can be rebased: %s", rsrc.getPatchSet());
       return description;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index d5bfea1..5e4ede3 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -50,13 +51,11 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Restore extends RetryingRestModifyView<ChangeResource, RestoreInput, ChangeInfo>
     implements UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Restore.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
@@ -153,7 +152,7 @@
         cm.setChangeMessage(message.getMessage(), ctx.getWhen());
         cm.send();
       } catch (Exception e) {
-        log.error("Cannot email update for change " + change.getId(), e);
+        logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
       }
       changeRestored.fire(
           change, patchSet, ctx.getAccount(), Strings.emptyToNull(input.message), ctx.getWhen());
@@ -178,7 +177,8 @@
         return description;
       }
     } catch (IOException e) {
-      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if project state permits write: %s", rsrc.getProject());
       return description;
     }
 
@@ -187,10 +187,8 @@
         return description;
       }
     } catch (OrmException | IOException e) {
-      log.error(
-          String.format(
-              "Failed to check if the current patch set of change %s is locked", change.getId()),
-          e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if the current patch set of change %s is locked", change.getId());
       return description;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index 4545794..55d0933 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -85,13 +86,11 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.ChangeIdUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput, ChangeInfo>
     implements UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Revert.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
@@ -265,7 +264,8 @@
     try {
       projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
     } catch (IOException e) {
-      log.error("Failed to check if project state permits write: " + rsrc.getProject(), e);
+      logger.atSevere().withCause(e).log(
+          "Failed to check if project state permits write: %s", rsrc.getProject());
     }
     return new UiAction.Description()
         .setLabel("Revert")
@@ -306,7 +306,8 @@
         cm.setAccountsToNotify(accountsToNotify);
         cm.send();
       } catch (Exception err) {
-        log.error("Cannot send email for revert change " + change.getId(), err);
+        logger.atSevere().withCause(err).log(
+            "Cannot send email for revert change %s", change.getId());
       }
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index dae37d6..47c6970 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -61,11 +62,10 @@
 import org.apache.commons.lang.mutable.MutableDouble;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ReviewerRecommender {
-  private static final Logger log = LoggerFactory.getLogger(ReviewerRecommender.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final double BASE_REVIEWER_WEIGHT = 10;
   private static final double BASE_OWNER_WEIGHT = 1;
   private static final double BASE_COMMENT_WEIGHT = 0.5;
@@ -140,11 +140,11 @@
       if (Strings.isNullOrEmpty(pluginWeight)) {
         pluginWeight = "1";
       }
-      log.debug("weight for {}: {}", key, pluginWeight);
+      logger.atFine().log("weight for %s: %s", key, pluginWeight);
       try {
         weights.add(Double.parseDouble(pluginWeight));
       } catch (NumberFormatException e) {
-        log.error("Exception while parsing weight for {}", key, e);
+        logger.atSevere().withCause(e).log("Exception while parsing weight for %s", key);
         weights.add(1d);
       }
     }
@@ -164,7 +164,7 @@
         }
       }
     } catch (ExecutionException | InterruptedException e) {
-      log.error("Exception while suggesting reviewers", e);
+      logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
       return ImmutableList.of();
     }
 
@@ -212,7 +212,7 @@
       return suggestions;
     } catch (QueryParseException e) {
       // Unhandled, because owner:self will never provoke a QueryParseException
-      log.error("Exception while suggesting reviewers", e);
+      logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
       return ImmutableMap.of();
     }
   }
@@ -254,7 +254,7 @@
       } catch (QueryParseException e) {
         // Unhandled: If an exception is thrown, we won't increase the
         // candidates's score
-        log.error("Exception while suggesting reviewers", e);
+        logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
       }
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 95557b5..65052a5 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.flogger.LazyArgs.lazy;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
@@ -21,6 +22,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.common.GroupBaseInfo;
@@ -66,10 +68,10 @@
 import java.util.Objects;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ReviewersUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   @Singleton
   private static class Metrics {
     final Timer0 queryAccountsLatency;
@@ -113,8 +115,6 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(ReviewersUtil.class);
-
   // Generate a candidate list at 2x the size of what the user wants to see to
   // give the ranking algorithm a good set of candidates it can work with
   private static final int CANDIDATE_LIST_MULTIPLIER = 2;
@@ -170,35 +170,33 @@
       throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
     CurrentUser currentUser = self.get();
     if (changeNotes != null) {
-      log.debug(
-          "Suggesting reviewers for change {} to user {}.",
-          changeNotes.getChangeId().get(),
-          currentUser.getLoggableName());
+      logger.atFine().log(
+          "Suggesting reviewers for change %s to user %s.",
+          changeNotes.getChangeId().get(), currentUser.getLoggableName());
     } else {
-      log.debug(
-          "Suggesting default reviewers for project {} to user {}.",
-          projectState.getName(),
-          currentUser.getLoggableName());
+      logger.atFine().log(
+          "Suggesting default reviewers for project %s to user %s.",
+          projectState.getName(), currentUser.getLoggableName());
     }
 
     String query = suggestReviewers.getQuery();
-    log.debug("Query: {}", query);
+    logger.atFine().log("Query: %s", query);
     int limit = suggestReviewers.getLimit();
 
     if (!suggestReviewers.getSuggestAccounts()) {
-      log.debug("Reviewer suggestion is disabled.");
+      logger.atFine().log("Reviewer suggestion is disabled.");
       return Collections.emptyList();
     }
 
     List<Account.Id> candidateList = new ArrayList<>();
     if (!Strings.isNullOrEmpty(query)) {
       candidateList = suggestAccounts(suggestReviewers);
-      log.debug("Candidate list: {}", candidateList);
+      logger.atFine().log("Candidate list: %s", candidateList);
     }
 
     List<Account.Id> sortedRecommendations =
         recommendAccounts(changeNotes, suggestReviewers, projectState, candidateList);
-    log.debug("Sorted recommendations: {}", sortedRecommendations);
+    logger.atFine().log("Sorted recommendations: %s", sortedRecommendations);
 
     // Filter accounts by visibility and enforce limit
     List<Account.Id> filteredRecommendations = new ArrayList<>();
@@ -214,39 +212,17 @@
         }
       }
     }
-    log.debug("Filtered recommendations: {}", filteredRecommendations);
+    logger.atFine().log("Filtered recommendations: %s", filteredRecommendations);
 
-    List<SuggestedReviewerInfo> suggestedReviewers = loadAccounts(filteredRecommendations);
-    if (!excludeGroups && suggestedReviewers.size() < limit && !Strings.isNullOrEmpty(query)) {
-      // Add groups at the end as individual accounts are usually more
-      // important.
-      suggestedReviewers.addAll(
-          suggestAccountGroups(
-              suggestReviewers,
-              projectState,
-              visibilityControl,
-              limit - suggestedReviewers.size()));
-    }
-
-    if (suggestedReviewers.size() > limit) {
-      suggestedReviewers = suggestedReviewers.subList(0, limit);
-      log.debug("Limited suggested reviewers to {} accounts.", limit);
-    }
-    log.debug(
-        "Suggested reviewers: {}",
-        suggestedReviewers
-            .stream()
-            .map(
-                r -> {
-                  if (r.account != null) {
-                    return "a/" + r.account._accountId;
-                  } else if (r.group != null) {
-                    return "g/" + r.group.id;
-                  } else {
-                    return "";
-                  }
-                })
-            .collect(toList()));
+    List<SuggestedReviewerInfo> suggestedReviewers =
+        suggestReviewers(
+            suggestReviewers,
+            projectState,
+            visibilityControl,
+            excludeGroups,
+            filteredRecommendations);
+    logger.atFine().log(
+        "Suggested reviewers: %s", lazy(() -> formatSuggestedReviewers(suggestedReviewers)));
     return suggestedReviewers;
   }
 
@@ -280,6 +256,36 @@
     }
   }
 
+  private List<SuggestedReviewerInfo> suggestReviewers(
+      SuggestReviewers suggestReviewers,
+      ProjectState projectState,
+      VisibilityControl visibilityControl,
+      boolean excludeGroups,
+      List<Account.Id> filteredRecommendations)
+      throws OrmException, PermissionBackendException, IOException {
+    List<SuggestedReviewerInfo> suggestedReviewers = loadAccounts(filteredRecommendations);
+
+    int limit = suggestReviewers.getLimit();
+    if (!excludeGroups
+        && suggestedReviewers.size() < limit
+        && !Strings.isNullOrEmpty(suggestReviewers.getQuery())) {
+      // Add groups at the end as individual accounts are usually more
+      // important.
+      suggestedReviewers.addAll(
+          suggestAccountGroups(
+              suggestReviewers,
+              projectState,
+              visibilityControl,
+              limit - suggestedReviewers.size()));
+    }
+
+    if (suggestedReviewers.size() > limit) {
+      suggestedReviewers = suggestedReviewers.subList(0, limit);
+      logger.atFine().log("Limited suggested reviewers to %d accounts.", limit);
+    }
+    return suggestedReviewers;
+  }
+
   private List<Account.Id> recommendAccounts(
       @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
@@ -411,4 +417,21 @@
 
     return result;
   }
+
+  private static String formatSuggestedReviewers(List<SuggestedReviewerInfo> suggestedReviewers) {
+    return suggestedReviewers
+        .stream()
+        .map(
+            r -> {
+              if (r.account != null) {
+                return "a/" + r.account._accountId;
+              } else if (r.group != null) {
+                return "g/" + r.group.id;
+              } else {
+                return "";
+              }
+            })
+        .collect(toList())
+        .toString();
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 54ecd18..a161767 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -79,13 +80,11 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Submit
     implements RestModifyView<RevisionResource, SubmitInput>, UiAction<RevisionResource> {
-  private static final Logger log = LoggerFactory.getLogger(Submit.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String DEFAULT_TOOLTIP = "Submit patch set ${patchSet} into ${branch}";
   private static final String DEFAULT_TOOLTIP_ANCESTORS =
@@ -95,14 +94,10 @@
       "Submit all ${topicSize} changes of the same topic "
           + "(${submitSize} changes including ancestors and other "
           + "changes related by topic)";
-  private static final String BLOCKED_SUBMIT_TOOLTIP =
-      "This change depends on other changes which are not ready";
   private static final String BLOCKED_HIDDEN_SUBMIT_TOOLTIP =
       "This change depends on other hidden changes which are not ready";
-  private static final String BLOCKED_WORK_IN_PROGRESS = "This change is marked work in progress";
   private static final String CLICK_FAILURE_TOOLTIP = "Clicking the button would fail";
   private static final String CHANGE_UNMERGEABLE = "Problems with integrating this change";
-  private static final String CHANGES_NOT_MERGEABLE = "Problems with change(s): ";
 
   public static class Output {
     transient Change change;
@@ -240,6 +235,11 @@
   }
 
   /**
+   * Returns a message describing what prevents the current change from being submitted - or null.
+   * This method only considers parent changes, and changes in the same topic. The caller is
+   * responsible for making sure the current change to be submitted can indeed be submitted
+   * (permissions, submit rules, is not a WIP...)
+   *
    * @param cd the change the user is currently looking at
    * @param cs set of changes to be submitted at once
    * @param user the user who is checking to submit
@@ -251,6 +251,11 @@
         return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
       }
       for (ChangeData c : cs.changes()) {
+        if (cd.getId().equals(c.getId())) {
+          // We ignore the change about to be submitted, as these checks are already done in the
+          // #apply and #getDescription methods.
+          continue;
+        }
         Set<ChangePermission> can =
             permissionBackend
                 .user(user)
@@ -261,12 +266,16 @@
           return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
         }
         if (!can.contains(ChangePermission.SUBMIT)) {
-          return BLOCKED_SUBMIT_TOOLTIP;
+          return "You don't have permission to submit change " + c.getId();
         }
         if (c.change().isWorkInProgress()) {
-          return BLOCKED_WORK_IN_PROGRESS;
+          return "Change " + c.getId() + " is marked work in progress";
         }
-        MergeOp.checkSubmitRule(c, false);
+        try {
+          MergeOp.checkSubmitRule(c, false);
+        } catch (ResourceConflictException e) {
+          return "Change " + c.getId() + " is not ready: " + e.getMessage();
+        }
       }
 
       Collection<ChangeData> unmergeable = unmergeableChanges(cs);
@@ -278,13 +287,12 @@
             return CHANGE_UNMERGEABLE;
           }
         }
-        return CHANGES_NOT_MERGEABLE
+
+        return "Problems with change(s): "
             + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", "));
       }
-    } catch (ResourceConflictException e) {
-      return BLOCKED_SUBMIT_TOOLTIP;
     } catch (PermissionBackendException | OrmException | IOException e) {
-      log.error("Error checking if change is submittable", e);
+      logger.atSevere().withCause(e).log("Error checking if change is submittable");
       throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
     return null;
@@ -294,6 +302,7 @@
   public UiAction.Description getDescription(RevisionResource resource) {
     Change change = resource.getChange();
     if (!change.getStatus().isOpen()
+        || change.isWorkInProgress()
         || !resource.isCurrent()
         || !resource.permissions().testOrFalse(ChangePermission.SUBMIT)) {
       return null; // submit not visible
@@ -304,7 +313,7 @@
         return null; // submit not visible
       }
     } catch (IOException e) {
-      log.error("Error checking if change is submittable", e);
+      logger.atSevere().withCause(e).log("Error checking if change is submittable");
       throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
 
@@ -315,7 +324,7 @@
     } catch (ResourceConflictException e) {
       return null; // submit not visible
     } catch (OrmException e) {
-      log.error("Error checking if change is submittable", e);
+      logger.atSevere().withCause(e).log("Error checking if change is submittable");
       throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index bbfe75d..4ced4c2 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -18,6 +18,7 @@
 import static java.util.Collections.reverseOrder;
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -47,11 +48,9 @@
 import java.util.EnumSet;
 import java.util.List;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SubmittedTogether implements RestReadView<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(SubmittedTogether.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final EnumSet<SubmittedTogetherOption> options =
       EnumSet.noneOf(SubmittedTogetherOption.class);
@@ -155,7 +154,7 @@
       info.nonVisibleChanges = hidden;
       return info;
     } catch (OrmException | IOException e) {
-      log.error("Error on getting a ChangeSet", e);
+      logger.atSevere().withCause(e).log("Error on getting a ChangeSet");
       throw e;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 2dac5ef..bc3dfa7 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -44,10 +44,9 @@
     implements RestReadView<ChangeResource> {
 
   @Option(
-    name = "--exclude-groups",
-    aliases = {"-e"},
-    usage = "exclude groups from query"
-  )
+      name = "--exclude-groups",
+      aliases = {"-e"},
+      usage = "exclude groups from query")
   boolean excludeGroups;
 
   private final PermissionBackend permissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index d32abe8..ac8e81c 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -42,21 +42,19 @@
   protected final int maxSuggestedReviewers;
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of reviewers to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of reviewers to list")
   public void setLimit(int l) {
     this.limit = l <= 0 ? maxSuggestedReviewers : Math.min(l, maxSuggestedReviewers);
   }
 
   @Option(
-    name = "--query",
-    aliases = {"-q"},
-    metaVar = "QUERY",
-    usage = "match reviewers query"
-  )
+      name = "--query",
+      aliases = {"-q"},
+      metaVar = "QUERY",
+      usage = "match reviewers query")
   public void setQuery(String q) {
     this.query = q;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Unignore.java b/java/com/google/gerrit/server/restapi/change/Unignore.java
index d1be312..6f2144a 100644
--- a/java/com/google/gerrit/server/restapi/change/Unignore.java
+++ b/java/com/google/gerrit/server/restapi/change/Unignore.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -24,12 +25,10 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Unignore implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Unignore.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final StarredChangesUtil stars;
 
@@ -59,7 +58,7 @@
     try {
       return stars.isIgnored(rsrc);
     } catch (OrmException e) {
-      log.error("failed to check ignored star", e);
+      logger.atSevere().withCause(e).log("failed to check ignored star");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/config/AgreementJson.java b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
index 3ad965d..548bc03 100644
--- a/java/com/google/gerrit/server/restapi/config/AgreementJson.java
+++ b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -26,11 +27,9 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class AgreementJson {
-  private static final Logger log = LoggerFactory.getLogger(AgreementJson.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<CurrentUser> self;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
@@ -62,12 +61,9 @@
         GroupResource group = new GroupResource(gc);
         info.autoVerifyGroup = groupJson.format(group);
       } catch (NoSuchGroupException | OrmException e) {
-        log.warn(
-            "autoverify group \""
-                + autoVerifyGroup.getName()
-                + "\" does not exist, referenced in CLA \""
-                + ca.getName()
-                + "\"");
+        logger.atWarning().log(
+            "autoverify group \"%s\" does not exist, referenced in CLA \"%s\"",
+            autoVerifyGroup.getName(), ca.getName());
       }
     }
     return info;
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index 412b88d..fa9bfde 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.config;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -29,13 +30,12 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Pattern;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** List capabilities visible to the calling user. */
 @Singleton
 public class ListCapabilities implements RestReadView<ConfigResource> {
-  private static final Logger log = LoggerFactory.getLogger(ListCapabilities.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final Pattern PLUGIN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
 
   private final PermissionBackend permissionBackend;
@@ -62,10 +62,9 @@
     Map<String, CapabilityInfo> output = new HashMap<>();
     for (String pluginName : pluginCapabilities.plugins()) {
       if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
-        log.warn(
-            "Plugin name '{}' must match '{}' to use capabilities; rename the plugin",
-            pluginName,
-            PLUGIN_NAME_PATTERN.pattern());
+        logger.atWarning().log(
+            "Plugin name '%s' must match '%s' to use capabilities; rename the plugin",
+            pluginName, PLUGIN_NAME_PATTERN.pattern());
         continue;
       }
       for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
diff --git a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
index 5e549c2..068f332 100644
--- a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -32,13 +33,11 @@
 import java.io.IOException;
 import java.lang.reflect.Field;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
 public class SetDiffPreferences implements RestModifyView<ConfigResource, DiffPreferencesInfo> {
-  private static final Logger log = LoggerFactory.getLogger(SetDiffPreferences.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
@@ -82,7 +81,7 @@
         }
       }
     } catch (IllegalAccessException e) {
-      log.warn("Unable to verify input", e);
+      logger.atWarning().withCause(e).log("Unable to verify input");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
index 8877239..daca734 100644
--- a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
@@ -32,13 +33,11 @@
 import java.io.IOException;
 import java.lang.reflect.Field;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
 public class SetEditPreferences implements RestModifyView<ConfigResource, EditPreferencesInfo> {
-  private static final Logger log = LoggerFactory.getLogger(SetDiffPreferences.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
@@ -82,7 +81,7 @@
         }
       }
     } catch (IllegalAccessException e) {
-      log.warn("Unable to verify input", e);
+      logger.atSevere().withCause(e).log("Unable to verify input");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/config/SetPreferences.java b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
index ce66ecc..6a0c22b 100644
--- a/java/com/google/gerrit/server/restapi/config/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -32,13 +33,11 @@
 import java.io.IOException;
 import java.lang.reflect.Field;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
 public class SetPreferences implements RestModifyView<ConfigResource, GeneralPreferencesInfo> {
-  private static final Logger log = LoggerFactory.getLogger(SetPreferences.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
@@ -79,7 +78,7 @@
         }
       }
     } catch (IllegalAccessException e) {
-      log.warn("Unable to verify input", e);
+      logger.atSevere().withCause(e).log("Unable to verify input");
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/restapi/config/TopMenuCollection.java b/java/com/google/gerrit/server/restapi/config/TopMenuCollection.java
index 36a1b04..cca1475 100644
--- a/java/com/google/gerrit/server/restapi/config/TopMenuCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TopMenuCollection.java
@@ -25,7 +25,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class TopMenuCollection implements ChildCollection<ConfigResource, TopMenuResource> {
+public class TopMenuCollection implements ChildCollection<ConfigResource, TopMenuResource> {
   private final DynamicMap<RestView<TopMenuResource>> views;
   private final ListTopMenus list;
 
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index 9ddafe3..461989d 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -211,12 +211,12 @@
     return result;
   }
 
-  static class PutMember implements RestModifyView<GroupResource, Input> {
+  public static class PutMember implements RestModifyView<GroupResource, Input> {
 
     private final AddMembers put;
     private final String id;
 
-    PutMember(AddMembers put, String id) {
+    public PutMember(AddMembers put, String id) {
       this.put = put;
       this.id = id;
     }
@@ -240,11 +240,11 @@
   }
 
   @Singleton
-  static class UpdateMember implements RestModifyView<MemberResource, Input> {
+  public static class UpdateMember implements RestModifyView<MemberResource, Input> {
     private final GetMember get;
 
     @Inject
-    UpdateMember(GetMember get) {
+    public UpdateMember(GetMember get) {
       this.get = get;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index e11f389..d0be5ac 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -127,12 +127,12 @@
     groupsUpdateProvider.get().updateGroup(parentGroupUuid, groupUpdate);
   }
 
-  static class PutSubgroup implements RestModifyView<GroupResource, Input> {
+  public static class PutSubgroup implements RestModifyView<GroupResource, Input> {
 
     private final AddSubgroups addSubgroups;
     private final String id;
 
-    PutSubgroup(AddSubgroups addSubgroups, String id) {
+    public PutSubgroup(AddSubgroups addSubgroups, String id) {
       this.addSubgroups = addSubgroups;
       this.id = id;
     }
@@ -156,11 +156,11 @@
   }
 
   @Singleton
-  static class UpdateSubgroup implements RestModifyView<SubgroupResource, Input> {
+  public static class UpdateSubgroup implements RestModifyView<SubgroupResource, Input> {
     private final Provider<GetSubgroup> get;
 
     @Inject
-    UpdateSubgroup(Provider<GetSubgroup> get) {
+    public UpdateSubgroup(Provider<GetSubgroup> get) {
       this.get = get;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index 3685469..bcacb65 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -92,12 +92,12 @@
   }
 
   @Singleton
-  static class DeleteMember implements RestModifyView<MemberResource, Input> {
+  public static class DeleteMember implements RestModifyView<MemberResource, Input> {
 
     private final Provider<DeleteMembers> delete;
 
     @Inject
-    DeleteMember(Provider<DeleteMembers> delete) {
+    public DeleteMember(Provider<DeleteMembers> delete) {
       this.delete = delete;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index 0eba8c7..934698b 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -96,12 +96,12 @@
   }
 
   @Singleton
-  static class DeleteSubgroup implements RestModifyView<SubgroupResource, Input> {
+  public static class DeleteSubgroup implements RestModifyView<SubgroupResource, Input> {
 
     private final Provider<DeleteSubgroups> delete;
 
     @Inject
-    DeleteSubgroup(Provider<DeleteSubgroups> delete) {
+    public DeleteSubgroup(Provider<DeleteSubgroups> delete) {
       this.delete = delete;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
index 38b22a9..d05194f 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -58,7 +58,7 @@
   private boolean hasQuery2;
 
   @Inject
-  GroupsCollection(
+  public GroupsCollection(
       DynamicMap<RestView<GroupResource>> views,
       Provider<ListGroups> list,
       Provider<QueryGroups> queryGroups,
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 0dbd7b6..c7f1d5e 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -95,37 +95,33 @@
   private String ownedBy;
 
   @Option(
-    name = "--project",
-    aliases = {"-p"},
-    usage = "projects for which the groups should be listed"
-  )
+      name = "--project",
+      aliases = {"-p"},
+      usage = "projects for which the groups should be listed")
   public void addProject(ProjectState project) {
     projects.add(project);
   }
 
   @Option(
-    name = "--visible-to-all",
-    usage = "to list only groups that are visible to all registered users"
-  )
+      name = "--visible-to-all",
+      usage = "to list only groups that are visible to all registered users")
   public void setVisibleToAll(boolean visibleToAll) {
     this.visibleToAll = visibleToAll;
   }
 
   @Option(
-    name = "--user",
-    aliases = {"-u"},
-    usage = "user for which the groups should be listed"
-  )
+      name = "--user",
+      aliases = {"-u"},
+      usage = "user for which the groups should be listed")
   public void setUser(Account.Id user) {
     this.user = user;
   }
 
   @Option(
-    name = "--owned",
-    usage =
-        "to list only groups that are owned by the"
-            + " specified user or by the calling user if no user was specifed"
-  )
+      name = "--owned",
+      usage =
+          "to list only groups that are owned by the"
+              + " specified user or by the calling user if no user was specifed")
   public void setOwned(boolean owned) {
     this.owned = owned;
   }
@@ -138,68 +134,61 @@
    */
   @Deprecated
   @Option(
-    name = "--query",
-    aliases = {"-q"},
-    usage = "group to inspect (deprecated: use --group/-g instead)"
-  )
+      name = "--query",
+      aliases = {"-q"},
+      usage = "group to inspect (deprecated: use --group/-g instead)")
   void addGroup_Deprecated(AccountGroup.UUID uuid) {
     addGroup(uuid);
   }
 
   @Option(
-    name = "--group",
-    aliases = {"-g"},
-    usage = "group to inspect"
-  )
+      name = "--group",
+      aliases = {"-g"},
+      usage = "group to inspect")
   public void addGroup(AccountGroup.UUID uuid) {
     groupsToInspect.add(uuid);
   }
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of groups to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of groups to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "number of groups to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "number of groups to skip")
   public void setStart(int start) {
     this.start = start;
   }
 
   @Option(
-    name = "--match",
-    aliases = {"-m"},
-    metaVar = "MATCH",
-    usage = "match group substring"
-  )
+      name = "--match",
+      aliases = {"-m"},
+      metaVar = "MATCH",
+      usage = "match group substring")
   public void setMatchSubstring(String matchSubstring) {
     this.matchSubstring = matchSubstring;
   }
 
   @Option(
-    name = "--regex",
-    aliases = {"-r"},
-    metaVar = "REGEX",
-    usage = "match group regex"
-  )
+      name = "--regex",
+      aliases = {"-r"},
+      metaVar = "REGEX",
+      usage = "match group regex")
   public void setMatchRegex(String matchRegex) {
     this.matchRegex = matchRegex;
   }
 
   @Option(
-    name = "--suggest",
-    aliases = {"-s"},
-    usage = "to get a suggestion of groups"
-  )
+      name = "--suggest",
+      aliases = {"-s"},
+      usage = "to get a suggestion of groups")
   public void setSuggest(String suggest) {
     this.suggest = suggest;
   }
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index a268f28..835a613 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Strings.nullToEmpty;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
@@ -30,11 +31,10 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import org.slf4j.Logger;
 
 @Singleton
 public class ListSubgroups implements RestReadView<GroupResource> {
-  private static final Logger log = org.slf4j.LoggerFactory.getLogger(ListSubgroups.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GroupControl.Factory controlFactory;
   private final GroupJson json;
@@ -64,9 +64,8 @@
           included.add(json.format(i.getGroup()));
         }
       } catch (NoSuchGroupException notFound) {
-        log.warn(
-            String.format(
-                "Group %s no longer available, subgroup of %s", subgroupUuid, group.getName()));
+        logger.atWarning().log(
+            "Group %s no longer available, subgroup of %s", subgroupUuid, group.getName());
         continue;
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index df04a2c..c262003 100644
--- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -49,30 +49,27 @@
   // removed we want to rename --query2 to --query here.
   /** --query (-q) is already used by {@link ListGroups} */
   @Option(
-    name = "--query2",
-    aliases = {"-q2"},
-    usage = "group query"
-  )
+      name = "--query2",
+      aliases = {"-q2"},
+      usage = "group query")
   public void setQuery(String query) {
     this.query = query;
   }
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of groups to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of groups to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "number of groups to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "number of groups to skip")
   public void setStart(int start) {
     this.start = start;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
index dd1c9a5..de2ac64 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
@@ -43,23 +43,21 @@
   private SubmitType submitType;
 
   @Option(
-    name = "--source",
-    metaVar = "COMMIT",
-    usage =
-        "the source reference to merge, which could be any git object "
-            + "references expression, refer to "
-            + "org.eclipse.jgit.lib.Repository#resolve(String)",
-    required = true
-  )
+      name = "--source",
+      metaVar = "COMMIT",
+      usage =
+          "the source reference to merge, which could be any git object "
+              + "references expression, refer to "
+              + "org.eclipse.jgit.lib.Repository#resolve(String)",
+      required = true)
   public void setSource(String source) {
     this.source = source;
   }
 
   @Option(
-    name = "--strategy",
-    metaVar = "STRATEGY",
-    usage = "name of the merge strategy, refer to org.eclipse.jgit.merge.MergeStrategy"
-  )
+      name = "--strategy",
+      metaVar = "STRATEGY",
+      usage = "name of the merge strategy, refer to org.eclipse.jgit.merge.MergeStrategy")
   public void setStrategy(String strategy) {
     this.strategy = strategy;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
index d43edfb..3855b78 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
@@ -27,7 +27,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 @Singleton
-class CommitIncludedIn implements RestReadView<CommitResource> {
+public class CommitIncludedIn implements RestReadView<CommitResource> {
   private IncludedIn includedIn;
 
   @Inject
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 87b5343..15cd824 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -41,12 +42,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class CommitsCollection implements ChildCollection<ProjectResource, CommitResource> {
-  private static final Logger log = LoggerFactory.getLogger(CommitsCollection.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
@@ -118,7 +117,8 @@
           return true;
         }
       } catch (OrmException e) {
-        log.error("Cannot look up change for commit " + commit.name() + " in " + project, e);
+        logger.atSevere().withCause(e).log(
+            "Cannot look up change for commit %s in %s", commit.name(), project);
       }
     }
 
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 6305d5d..0296c9c 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -48,11 +49,9 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class CreateBranch implements RestModifyView<ProjectResource, BranchInput> {
-  private static final Logger log = LoggerFactory.getLogger(CreateBranch.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     CreateBranch create(String ref);
@@ -193,7 +192,7 @@
         }
         return info;
       } catch (IOException err) {
-        log.error("Cannot create branch \"" + name + "\"", err);
+        logger.atSevere().withCause(err).log("Cannot create branch \"%s\"", name);
         throw err;
       }
     } catch (RefUtil.InvalidRevisionException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index d19b0fb..3a9a0e7 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -19,6 +19,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -86,17 +87,15 @@
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 public class CreateProject implements RestModifyView<TopLevelResource, ProjectInput> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public interface Factory {
     CreateProject create(String name);
   }
 
-  private static final Logger log = LoggerFactory.getLogger(CreateProject.class);
-
   private final Provider<ProjectsCollection> projectsCollection;
   private final Provider<GroupsCollection> groupsCollection;
   private final DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
@@ -275,7 +274,7 @@
       throw new BadRequestException("invalid project name: " + nameKey);
     } catch (ConfigInvalidException e) {
       String msg = "Cannot create " + nameKey;
-      log.error(msg, e);
+      logger.atSevere().withCause(e).log(msg);
       throw e;
     }
   }
@@ -383,7 +382,7 @@
         }
       }
     } catch (IOException e) {
-      log.error("Cannot create empty commit for " + project.get(), e);
+      logger.atSevere().withCause(e).log("Cannot create empty commit for %s", project.get());
       throw e;
     }
   }
@@ -398,7 +397,7 @@
       try {
         l.onNewProjectCreated(event);
       } catch (RuntimeException e) {
-        log.warn("Failure in NewProjectCreatedListener", e);
+        logger.atWarning().withCause(e).log("Failure in NewProjectCreatedListener");
       }
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index 2c3735f..b09d870 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -17,6 +17,7 @@
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
@@ -50,11 +51,9 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class CreateTag implements RestModifyView<ProjectResource, TagInput> {
-  private static final Logger log = LoggerFactory.getLogger(CreateTag.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     CreateTag create(String ref);
@@ -151,7 +150,7 @@
     } catch (InvalidRevisionException e) {
       throw new BadRequestException("Invalid base revision");
     } catch (GitAPIException e) {
-      log.error("Cannot create tag \"" + ref + "\"", e);
+      logger.atSevere().withCause(e).log("Cannot create tag \"%s\"", ref);
       throw new IOException(e);
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java b/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java
index 0aa5752..b9b69b2 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteDashboard.java
@@ -28,7 +28,7 @@
 import java.io.IOException;
 
 @Singleton
-class DeleteDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
+public class DeleteDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
   private final Provider<SetDefaultDashboard> defaultSetter;
 
   @Inject
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 13b21c9..769eaf8 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -21,6 +21,7 @@
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -50,11 +51,9 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class DeleteRef {
-  private static final Logger log = LoggerFactory.getLogger(DeleteRef.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final int MAX_LOCK_FAILURE_CALLS = 10;
   private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
@@ -138,7 +137,7 @@
       } catch (LockFailedException e) {
         result = RefUpdate.Result.LOCK_FAILURE;
       } catch (IOException e) {
-        log.error("Cannot delete " + ref, e);
+        logger.atSevere().withCause(e).log("Cannot delete %s", ref);
         throw e;
       }
       if (result == RefUpdate.Result.LOCK_FAILURE && --remainingLockFailureCalls > 0) {
@@ -162,7 +161,7 @@
         break;
 
       case REJECTED_CURRENT_BRANCH:
-        log.error("Cannot delete " + ref + ": " + result.name());
+        logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
         throw new ResourceConflictException("cannot delete current branch");
 
       case IO_FAILURE:
@@ -173,7 +172,7 @@
       case REJECTED_MISSING_OBJECT:
       case REJECTED_OTHER_REASON:
       default:
-        log.error("Cannot delete " + ref + ": " + result.name());
+        logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
         throw new ResourceConflictException("cannot delete: " + result.name());
     }
   }
@@ -277,7 +276,7 @@
         msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
         break;
     }
-    log.error(msg);
+    logger.atSevere().log(msg);
     errorMessages.append(msg);
     errorMessages.append("\n");
   }
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 0bcb892..6a50c2f 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -23,6 +23,7 @@
 
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.Permission;
@@ -67,12 +68,10 @@
 import java.util.Map;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class GetAccess implements RestReadView<ProjectResource> {
-  private static final Logger LOG = LoggerFactory.getLogger(GetAccess.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final ImmutableBiMap<PermissionRule.Action, PermissionRuleInfo.Action> ACTION_TYPE =
       ImmutableBiMap.of(
@@ -290,7 +289,7 @@
         group.name = basic.getName();
         group.url = basic.getUrl();
       } else {
-        LOG.warn("no such group: " + id);
+        logger.atWarning().log("no such group: %s", id);
         group = null;
       }
       groups.put(id, group);
diff --git a/java/com/google/gerrit/server/restapi/project/GetProject.java b/java/com/google/gerrit/server/restapi/project/GetProject.java
index a1b2fb1..26159e4 100644
--- a/java/com/google/gerrit/server/restapi/project/GetProject.java
+++ b/java/com/google/gerrit/server/restapi/project/GetProject.java
@@ -22,7 +22,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class GetProject implements RestReadView<ProjectResource> {
+public class GetProject implements RestReadView<ProjectResource> {
 
   private final ProjectJson json;
 
diff --git a/java/com/google/gerrit/server/restapi/project/GetReflog.java b/java/com/google/gerrit/server/restapi/project/GetReflog.java
index 9bd6955..4b9a489 100644
--- a/java/com/google/gerrit/server/restapi/project/GetReflog.java
+++ b/java/com/google/gerrit/server/restapi/project/GetReflog.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.ReflogEntryInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -36,47 +37,42 @@
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.Repository;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class GetReflog implements RestReadView<BranchResource> {
-  private static final Logger log = LoggerFactory.getLogger(GetReflog.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of reflog entries to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of reflog entries to list")
   public GetReflog setLimit(int limit) {
     this.limit = limit;
     return this;
   }
 
   @Option(
-    name = "--from",
-    metaVar = "TIMESTAMP",
-    usage =
-        "timestamp from which the reflog entries should be listed (UTC, format: "
-            + TimestampHandler.TIMESTAMP_FORMAT
-            + ")"
-  )
+      name = "--from",
+      metaVar = "TIMESTAMP",
+      usage =
+          "timestamp from which the reflog entries should be listed (UTC, format: "
+              + TimestampHandler.TIMESTAMP_FORMAT
+              + ")")
   public GetReflog setFrom(Timestamp from) {
     this.from = from;
     return this;
   }
 
   @Option(
-    name = "--to",
-    metaVar = "TIMESTAMP",
-    usage =
-        "timestamp until which the reflog entries should be listed (UTC, format: "
-            + TimestampHandler.TIMESTAMP_FORMAT
-            + ")"
-  )
+      name = "--to",
+      metaVar = "TIMESTAMP",
+      usage =
+          "timestamp until which the reflog entries should be listed (UTC, format: "
+              + TimestampHandler.TIMESTAMP_FORMAT
+              + ")")
   public GetReflog setTo(Timestamp to) {
     this.to = to;
     return this;
@@ -106,7 +102,7 @@
         r = repo.getReflogReader(rsrc.getRef());
       } catch (UnsupportedOperationException e) {
         String msg = "reflog not supported on repo " + rsrc.getNameKey().get();
-        log.error(msg);
+        logger.atSevere().log(msg);
         throw new MethodNotAllowedException(msg);
       }
       if (r == null) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index ed9dede..6417967 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -64,41 +64,37 @@
   private final WebLinks webLinks;
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of branches to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of branches to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S", "-s"},
-    metaVar = "CNT",
-    usage = "number of branches to skip"
-  )
+      name = "--start",
+      aliases = {"-S", "-s"},
+      metaVar = "CNT",
+      usage = "number of branches to skip")
   public void setStart(int start) {
     this.start = start;
   }
 
   @Option(
-    name = "--match",
-    aliases = {"-m"},
-    metaVar = "MATCH",
-    usage = "match branches substring"
-  )
+      name = "--match",
+      aliases = {"-m"},
+      metaVar = "MATCH",
+      usage = "match branches substring")
   public void setMatchSubstring(String matchSubstring) {
     this.matchSubstring = matchSubstring;
   }
 
   @Option(
-    name = "--regex",
-    aliases = {"-r"},
-    metaVar = "REGEX",
-    usage = "match branches regex"
-  )
+      name = "--regex",
+      aliases = {"-r"},
+      metaVar = "REGEX",
+      usage = "match branches regex")
   public void setMatchRegex(String matchRegex) {
     this.matchRegex = matchRegex;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 882e922..0f6b54f 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -43,11 +44,9 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ListDashboards implements RestReadView<ProjectResource> {
-  private static final Logger log = LoggerFactory.getLogger(ListDashboards.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager gitManager;
   private final PermissionBackend permissionBackend;
@@ -139,10 +138,9 @@
                     project,
                     setDefault));
           } catch (ConfigInvalidException e) {
-            log.warn(
-                String.format(
-                    "Cannot parse dashboard %s:%s:%s: %s",
-                    definingProject.getName(), ref.getName(), tw.getPathString(), e.getMessage()));
+            logger.atWarning().log(
+                "Cannot parse dashboard %s:%s:%s: %s",
+                definingProject.getName(), ref.getName(), tw.getPathString(), e.getMessage());
           }
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 3407d39..72a0788 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -79,12 +80,10 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** List projects visible to the calling user. */
 public class ListProjects implements RestReadView<TopLevelResource> {
-  private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public enum FilterType {
     CODE {
@@ -154,21 +153,19 @@
   private OutputFormat format = OutputFormat.TEXT;
 
   @Option(
-    name = "--show-branch",
-    aliases = {"-b"},
-    usage = "displays the sha of each project in the specified branch"
-  )
+      name = "--show-branch",
+      aliases = {"-b"},
+      usage = "displays the sha of each project in the specified branch")
   public void addShowBranch(String branch) {
     showBranch.add(branch);
   }
 
   @Option(
-    name = "--tree",
-    aliases = {"-t"},
-    usage =
-        "displays project inheritance in a tree-like format\n"
-            + "this option does not work together with the show-branch option"
-  )
+      name = "--tree",
+      aliases = {"-t"},
+      usage =
+          "displays project inheritance in a tree-like format\n"
+              + "this option does not work together with the show-branch option")
   public void setShowTree(boolean showTree) {
     this.showTree = showTree;
   }
@@ -179,10 +176,9 @@
   }
 
   @Option(
-    name = "--description",
-    aliases = {"-d"},
-    usage = "include description of project in list"
-  )
+      name = "--description",
+      aliases = {"-d"},
+      usage = "include description of project in list")
   public void setShowDescription(boolean showDescription) {
     this.showDescription = showDescription;
   }
@@ -193,50 +189,45 @@
   }
 
   @Option(
-    name = "--state",
-    aliases = {"-s"},
-    usage = "filter by project state"
-  )
+      name = "--state",
+      aliases = {"-s"},
+      usage = "filter by project state")
   public void setState(com.google.gerrit.extensions.client.ProjectState state) {
     this.state = state;
   }
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of projects to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of projects to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "number of projects to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "number of projects to skip")
   public void setStart(int start) {
     this.start = start;
   }
 
   @Option(
-    name = "--prefix",
-    aliases = {"-p"},
-    metaVar = "PREFIX",
-    usage = "match project prefix"
-  )
+      name = "--prefix",
+      aliases = {"-p"},
+      metaVar = "PREFIX",
+      usage = "match project prefix")
   public void setMatchPrefix(String matchPrefix) {
     this.matchPrefix = matchPrefix;
   }
 
   @Option(
-    name = "--match",
-    aliases = {"-m"},
-    metaVar = "MATCH",
-    usage = "match project substring"
-  )
+      name = "--match",
+      aliases = {"-m"},
+      metaVar = "MATCH",
+      usage = "match project substring")
   public void setMatchSubstring(String matchSubstring) {
     this.matchSubstring = matchSubstring;
   }
@@ -247,10 +238,9 @@
   }
 
   @Option(
-    name = "--has-acl-for",
-    metaVar = "GROUP",
-    usage = "displays only projects on which access rights for this group are directly assigned"
-  )
+      name = "--has-acl-for",
+      metaVar = "GROUP",
+      usage = "displays only projects on which access rights for this group are directly assigned")
   public void setGroupUuid(AccountGroup.UUID groupUuid) {
     this.groupUuid = groupUuid;
   }
@@ -453,7 +443,7 @@
           // If the Git repository is gone, the project doesn't actually exist anymore.
           continue;
         } catch (IOException err) {
-          log.warn("Unexpected error reading " + projectName, err);
+          logger.atWarning().withCause(err).log("Unexpected error reading %s", projectName);
           continue;
         }
 
@@ -557,9 +547,8 @@
                   if (projectCache.get(parent) != null) {
                     return parent;
                   }
-                  log.warn(
-                      String.format(
-                          "parent project %s of project %s not found", parent.get(), ps.getName()));
+                  logger.atWarning().log(
+                      "parent project %s of project %s not found", parent.get(), ps.getName());
                 }
               }
               return null;
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index ec6a99b..e79fdca 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -60,41 +60,37 @@
   private final WebLinks links;
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of tags to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of tags to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S", "-s"},
-    metaVar = "CNT",
-    usage = "number of tags to skip"
-  )
+      name = "--start",
+      aliases = {"-S", "-s"},
+      metaVar = "CNT",
+      usage = "number of tags to skip")
   public void setStart(int start) {
     this.start = start;
   }
 
   @Option(
-    name = "--match",
-    aliases = {"-m"},
-    metaVar = "MATCH",
-    usage = "match tags substring"
-  )
+      name = "--match",
+      aliases = {"-m"},
+      metaVar = "MATCH",
+      usage = "match tags substring")
   public void setMatchSubstring(String matchSubstring) {
     this.matchSubstring = matchSubstring;
   }
 
   @Option(
-    name = "--regex",
-    aliases = {"-r"},
-    metaVar = "REGEX",
-    usage = "match tags regex"
-  )
+      name = "--regex",
+      aliases = {"-r"},
+      metaVar = "REGEX",
+      usage = "match tags regex")
   public void setMatchRegex(String matchRegex) {
     this.matchRegex = matchRegex;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectNode.java b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
index 54f7574..c83e473 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectNode.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
@@ -23,8 +23,8 @@
 import java.util.TreeSet;
 
 /** Node of a Project in a tree formatted by {@link ListProjects}. */
-class ProjectNode implements TreeNode, Comparable<ProjectNode> {
-  interface Factory {
+public class ProjectNode implements TreeNode, Comparable<ProjectNode> {
+  public interface Factory {
     ProjectNode create(Project project, boolean isVisible);
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index ca7eb06..db596e6 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
@@ -58,12 +59,11 @@
 import java.util.regex.Pattern;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
-  private static final Logger log = LoggerFactory.getLogger(PutConfig.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final Pattern PARAMETER_NAME_PATTERN =
       Pattern.compile("^[a-zA-Z0-9]+[a-zA-Z0-9-]*$");
 
@@ -164,7 +164,7 @@
           throw new ResourceConflictException(
               "Cannot update " + projectName + ": " + e.getCause().getMessage());
         }
-        log.warn(String.format("Failed to update config of project %s.", projectName), e);
+        logger.atWarning().withCause(e).log("Failed to update config of project %s.", projectName);
         throw new ResourceConflictException("Cannot update " + projectName);
       }
 
@@ -201,10 +201,9 @@
         if (projectConfigEntry != null) {
           if (!PARAMETER_NAME_PATTERN.matcher(v.getKey()).matches()) {
             // TODO check why we have this restriction
-            log.warn(
-                "Parameter name '{}' must match '{}'",
-                v.getKey(),
-                PARAMETER_NAME_PATTERN.pattern());
+            logger.atWarning().log(
+                "Parameter name '%s' must match '%s'",
+                v.getKey(), PARAMETER_NAME_PATTERN.pattern());
             continue;
           }
           String oldValue = cfg.getString(v.getKey());
@@ -252,10 +251,9 @@
                     cfg.setStringList(v.getKey(), v.getValue().values);
                     break;
                   default:
-                    log.warn(
-                        String.format(
-                            "The type '%s' of parameter '%s' is not supported.",
-                            projectConfigEntry.getType().name(), v.getKey()));
+                    logger.atWarning().log(
+                        "The type '%s' of parameter '%s' is not supported.",
+                        projectConfigEntry.getType().name(), v.getKey());
                 }
               } catch (NumberFormatException ex) {
                 throw new BadRequestException(
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index 64adb0d..1e094a0 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -46,30 +46,27 @@
   private int start;
 
   @Option(
-    name = "--query",
-    aliases = {"-q"},
-    usage = "project query"
-  )
+      name = "--query",
+      aliases = {"-q"},
+      usage = "project query")
   public void setQuery(String query) {
     this.query = query;
   }
 
   @Option(
-    name = "--limit",
-    aliases = {"-n"},
-    metaVar = "CNT",
-    usage = "maximum number of projects to list"
-  )
+      name = "--limit",
+      aliases = {"-n"},
+      metaVar = "CNT",
+      usage = "maximum number of projects to list")
   public void setLimit(int limit) {
     this.limit = limit;
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    metaVar = "CNT",
-    usage = "number of projects to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      metaVar = "CNT",
+      usage = "number of projects to skip")
   public void setStart(int start) {
     this.start = start;
   }
diff --git a/java/com/google/gerrit/server/restapi/project/SetHead.java b/java/com/google/gerrit/server/restapi/project/SetHead.java
index aa1bf63..feff98e 100644
--- a/java/com/google/gerrit/server/restapi/project/SetHead.java
+++ b/java/com/google/gerrit/server/restapi/project/SetHead.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -42,12 +43,10 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class SetHead implements RestModifyView<ProjectResource, HeadInput> {
-  private static final Logger log = LoggerFactory.getLogger(SetHead.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final Provider<IdentifiedUser> identifiedUser;
@@ -128,7 +127,7 @@
       try {
         l.onHeadUpdated(event);
       } catch (RuntimeException e) {
-        log.warn("Failure in HeadUpdatedListener", e);
+        logger.atWarning().withCause(e).log("Failure in HeadUpdatedListener");
       }
     }
   }
diff --git a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
index a9482fe..65ac88f 100644
--- a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
+++ b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -33,8 +34,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Java implementation of Gerrit's default pre-submit rules behavior: check if the labels have the
@@ -45,7 +44,7 @@
  */
 @Singleton
 public final class DefaultSubmitRule implements SubmitRule {
-  private static final Logger log = LoggerFactory.getLogger(DefaultSubmitRule.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends FactoryModule {
     @Override
@@ -82,7 +81,8 @@
       labelTypes = cd.getLabelTypes().getLabelTypes();
       approvals = cd.currentApprovals();
     } catch (OrmException e) {
-      log.error("Unable to fetch labels and approvals for change {}", cd.getId(), e);
+      logger.atSevere().withCause(e).log(
+          "Unable to fetch labels and approvals for change %s", cd.getId());
 
       submitRecord.errorMessage = "Unable to fetch labels and approvals for the change";
       submitRecord.status = SubmitRecord.Status.RULE_ERROR;
@@ -94,8 +94,8 @@
     for (LabelType t : labelTypes) {
       LabelFunction labelFunction = t.getFunction();
       if (labelFunction == null) {
-        log.error(
-            "Unable to find the LabelFunction for label {}, change {}", t.getName(), cd.getId());
+        logger.atSevere().log(
+            "Unable to find the LabelFunction for label %s, change %s", t.getName(), cd.getId());
 
         submitRecord.errorMessage = "Unable to find the LabelFunction for label " + t.getName();
         submitRecord.status = SubmitRecord.Status.RULE_ERROR;
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 9dd0b86..083898b 100644
--- a/java/com/google/gerrit/server/rules/PrologEnvironment.java
+++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.rules;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -39,8 +40,6 @@
 import java.util.List;
 import java.util.Map;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Per-thread Prolog interpreter.
@@ -50,7 +49,7 @@
  * <p>A single copy of the Prolog interpreter, for the current thread.
  */
 public class PrologEnvironment extends BufferingPrologControl {
-  private static final Logger log = LoggerFactory.getLogger(PrologEnvironment.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     /**
@@ -142,7 +141,7 @@
       try {
         i.next().run();
       } catch (Throwable err) {
-        log.error("Failed to execute cleanup for PrologEnvironment", err);
+        logger.atSevere().withCause(err).log("Failed to execute cleanup for PrologEnvironment");
       }
       i.remove();
     }
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
index 0cc907d..5e2cfcc 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultRuleError;
 import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultTypeError;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -50,15 +51,13 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
  * the results through rules found in the parent projects, all the way up to All-Projects.
  */
 public class PrologRuleEvaluator {
-  private static final Logger log = LoggerFactory.getLogger(PrologRuleEvaluator.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     /** Returns a new {@link PrologRuleEvaluator} with the specified options */
@@ -302,11 +301,7 @@
 
   private List<SubmitRecord> ruleError(String err, Exception e) {
     if (opts.logErrors()) {
-      if (e == null) {
-        log.error(err);
-      } else {
-        log.error(err, e);
-      }
+      logger.atSevere().withCause(e).log(err);
       return defaultRuleError();
     }
     return createRuleError(err);
@@ -384,11 +379,7 @@
 
   private SubmitTypeRecord typeError(String err, Exception e) {
     if (opts.logErrors()) {
-      if (e == null) {
-        log.error(err);
-      } else {
-        log.error(err, e);
-      }
+      logger.atSevere().withCause(e).log(err);
       return defaultTypeError();
     }
     return SubmitTypeRecord.error(err);
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index 32a14db..44bede9 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -18,10 +18,10 @@
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:dbcp",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/log:jsonevent-layout",
         "//lib/log:log4j",
     ],
diff --git a/java/com/google/gerrit/server/schema/GroupBundle.java b/java/com/google/gerrit/server/schema/GroupBundle.java
index e15587b..58d3435 100644
--- a/java/com/google/gerrit/server/schema/GroupBundle.java
+++ b/java/com/google/gerrit/server/schema/GroupBundle.java
@@ -30,6 +30,7 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -60,8 +61,6 @@
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * A bundle of all entities rooted at a single {@link AccountGroup} entity.
@@ -71,7 +70,7 @@
  */
 @AutoValue
 abstract class GroupBundle {
-  private static final Logger log = LoggerFactory.getLogger(GroupBundle.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static {
     // Initialization-time checks that the column set hasn't changed since the
@@ -414,12 +413,9 @@
       // corrupt, and it's not clear if we can programmatically repair it. For migrating to NoteDb,
       // we'll try our best to recreate it, but no guarantees it will match the real sequence of
       // attempted operations, which is in any case lost in the mists of time.
-      log.warn(
-          "group {} in {} has duplicate {} entities: {}",
-          uuid,
-          source,
-          clazz.getSimpleName(),
-          iterable);
+      logger.atWarning().log(
+          "group %s in %s has duplicate %s entities: %s",
+          uuid, source, clazz.getSimpleName(), iterable);
     }
     return set;
   }
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index 43f39b2..83a0986 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -18,6 +18,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -41,18 +42,17 @@
 import javax.sql.DataSource;
 import org.apache.commons.dbcp.BasicDataSource;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public abstract class JdbcAccountPatchReviewStore
     implements AccountPatchReviewStore, LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String ACCOUNT_PATCH_REVIEW_DB = "accountPatchReviewDb";
   private static final String H2_DB = "h2";
   private static final String MARIADB = "mariadb";
   private static final String MYSQL = "mysql";
   private static final String POSTGRESQL = "postgresql";
   private static final String URL = "url";
-  private static final Logger log = LoggerFactory.getLogger(JdbcAccountPatchReviewStore.class);
 
   public static class Module extends LifecycleModule {
     private final Config cfg;
@@ -164,7 +164,7 @@
     try {
       createTableIfNotExists();
     } catch (OrmException e) {
-      log.error("Failed to create table to store account patch reviews", e);
+      logger.atSevere().withCause(e).log("Failed to create table to store account patch reviews");
     }
   }
 
diff --git a/java/com/google/gerrit/server/schema/Schema_154.java b/java/com/google/gerrit/server/schema/Schema_154.java
index 6447921..8dfd356 100644
--- a/java/com/google/gerrit/server/schema/Schema_154.java
+++ b/java/com/google/gerrit/server/schema/Schema_154.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -44,12 +45,11 @@
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Migrate accounts to NoteDb. */
 public class Schema_154 extends SchemaVersion {
-  private static final Logger log = LoggerFactory.getLogger(Schema_154.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String TABLE = "accounts";
   private static final ImmutableMap<String, AccountSetter> ACCOUNT_FIELDS_MAP =
       ImmutableMap.<String, AccountSetter>builder()
@@ -98,7 +98,7 @@
   private Set<Account> scanAccounts(ReviewDb db, ProgressMonitor pm) throws SQLException {
     Map<String, AccountSetter> fields = getFields(db);
     if (fields.isEmpty()) {
-      log.warn("Only account_id and registered_on fields are migrated for accounts");
+      logger.atWarning().log("Only account_id and registered_on fields are migrated for accounts");
     }
 
     List<String> queryFields = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/schema/Schema_167.java b/java/com/google/gerrit/server/schema/Schema_167.java
index 5e93b2c..ba93751 100644
--- a/java/com/google/gerrit/server/schema/Schema_167.java
+++ b/java/com/google/gerrit/server/schema/Schema_167.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
@@ -57,12 +58,10 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Migrate groups from ReviewDb to NoteDb. */
 public class Schema_167 extends SchemaVersion {
-  private static final Logger log = LoggerFactory.getLogger(Schema_167.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
@@ -196,11 +195,10 @@
         AccountConfig accountConfig = new AccountConfig(accountId, allUsersRepo).load();
         return accountConfig.getLoadedAccount();
       } catch (IOException | ConfigInvalidException ignored) {
-        log.warn(
-            "Failed to load account {}."
+        logger.atWarning().withCause(ignored).log(
+            "Failed to load account %s."
                 + " Cannot get account name for group audit log commit messages.",
-            accountId.get(),
-            ignored);
+            accountId.get());
         return Optional.empty();
       }
     }
@@ -248,11 +246,10 @@
         }
         return groupDescriptions;
       } catch (SQLException ignored) {
-        log.warn(
-            "Failed to load group {}."
+        logger.atWarning().withCause(ignored).log(
+            "Failed to load group %s."
                 + " Cannot get group name for group audit log commit messages.",
-            groupUuid.get(),
-            ignored);
+            groupUuid.get());
         return ImmutableList.of();
       }
     }
diff --git a/java/com/google/gerrit/server/securestore/SecureStoreProvider.java b/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
index 88c2072..4e43b2e 100644
--- a/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
+++ b/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.securestore;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.SiteLibraryLoaderUtil;
 import com.google.gerrit.server.config.SitePaths;
@@ -23,12 +24,10 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class SecureStoreProvider implements Provider<SecureStore> {
-  private static final Logger log = LoggerFactory.getLogger(SecureStoreProvider.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Path libdir;
   private final Injector injector;
@@ -58,7 +57,7 @@
       return (Class<? extends SecureStore>) Class.forName(className);
     } catch (ClassNotFoundException e) {
       String msg = String.format("Cannot load secure store class: %s", className);
-      log.error(msg, e);
+      logger.atSevere().withCause(e).log(msg);
       throw new RuntimeException(msg, e);
     }
   }
diff --git a/java/com/google/gerrit/server/ssh/SshAddressesModule.java b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
index 0e5b2f8..0a6bcac 100644
--- a/java/com/google/gerrit/server/ssh/SshAddressesModule.java
+++ b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.ssh;
 
 import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.AbstractModule;
@@ -26,11 +27,9 @@
 import java.util.Arrays;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SshAddressesModule extends AbstractModule {
-  private static final Logger log = LoggerFactory.getLogger(SshAddressesModule.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final int DEFAULT_PORT = 29418;
   public static final int IANA_SSH_PORT = 22;
@@ -57,7 +56,7 @@
       try {
         listen.add(SocketUtil.resolve(desc, DEFAULT_PORT));
       } catch (IllegalArgumentException e) {
-        log.error("Bad sshd.listenaddress: " + desc + ": " + e.getMessage());
+        logger.atSevere().log("Bad sshd.listenaddress: %s: %s", desc, e.getMessage());
       }
     }
     return listen;
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index aceb824..a6b73447 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -37,11 +38,9 @@
 import com.google.inject.assistedinject.Assisted;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class EmailMerge implements Runnable, RequestContext {
-  private static final Logger log = LoggerFactory.getLogger(EmailMerge.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   interface Factory {
     EmailMerge create(
@@ -107,7 +106,7 @@
       cm.setAccountsToNotify(accountsToNotify);
       cm.send();
     } catch (Exception e) {
-      log.error("Cannot email merged notification for " + changeId, e);
+      logger.atSevere().withCause(e).log("Cannot email merged notification for %s", changeId);
     } finally {
       requestContext.setContext(old);
       if (db != null) {
diff --git a/java/com/google/gerrit/server/submit/GitModules.java b/java/com/google/gerrit/server/submit/GitModules.java
index 92e0cb3..00ce7b2 100644
--- a/java/com/google/gerrit/server/submit/GitModules.java
+++ b/java/com/google/gerrit/server/submit/GitModules.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -36,15 +37,13 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.treewalk.TreeWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Loads the .gitmodules file of the specified project/branch. It can be queried which submodules
  * this branch is subscribed to.
  */
 public class GitModules {
-  private static final Logger log = LoggerFactory.getLogger(GitModules.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
     GitModules create(Branch.NameKey project, MergeOpRepoManager m);
@@ -63,7 +62,7 @@
       throws IOException {
     this.submissionId = orm.getSubmissionId();
     Project.NameKey project = branch.getParentKey();
-    logDebug("Loading .gitmodules of {} for project {}", branch, project);
+    logDebug("Loading .gitmodules of %s for project %s", branch, project);
     try {
       OpenRepo or = orm.getRepo(project);
       ObjectId id = or.repo.resolve(branch.get());
@@ -75,7 +74,7 @@
       try (TreeWalk tw = TreeWalk.forPath(or.repo, GIT_MODULES, commit.getTree())) {
         if (tw == null || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
           subscriptions = Collections.emptySet();
-          logDebug("The .gitmodules file doesn't exist in " + branch);
+          logDebug("The .gitmodules file doesn't exist in %s", branch);
           return;
         }
       }
@@ -93,20 +92,22 @@
   }
 
   public Collection<SubmoduleSubscription> subscribedTo(Branch.NameKey src) {
-    logDebug("Checking for a subscription of " + src);
+    logDebug("Checking for a subscription of %s", src);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     for (SubmoduleSubscription s : subscriptions) {
       if (s.getSubmodule().equals(src)) {
-        logDebug("Found " + s);
+        logDebug("Found %s", s);
         ret.add(s);
       }
     }
     return ret;
   }
 
-  private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(submissionId + msg, args);
-    }
+  private void logDebug(String msg, @Nullable Object arg) {
+    logger.atFine().log(submissionId + msg, arg);
+  }
+
+  private void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+    logger.atFine().log(submissionId + msg, arg1, arg2);
   }
 }
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index fa20ad9..06f57b5 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -55,15 +56,13 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Default implementation of MergeSuperSet that does the computation of the merge super set
  * sequentially on the local Gerrit instance.
  */
 public class LocalMergeSuperSetComputation implements MergeSuperSetComputation {
-  private static final Logger log = LoggerFactory.getLogger(LocalMergeSuperSetComputation.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends AbstractModule {
     @Override
@@ -261,9 +260,7 @@
   }
 
   private void logErrorAndThrow(String msg) throws OrmException {
-    if (log.isErrorEnabled()) {
-      log.error(msg);
-    }
+    logger.atSevere().log(msg);
     throw new OrmException(msg);
   }
 }
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 7dd1ac9..dedf764 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -31,6 +31,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -96,8 +97,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Merges changes in submission order into a single branch.
@@ -111,7 +110,7 @@
  * conflicting, even if an earlier commit along that same line can be merged cleanly.
  */
 public class MergeOp implements AutoCloseable {
-  private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.builder().build();
   private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_ALLOW_CLOSED =
@@ -160,12 +159,12 @@
 
     public void logProblem(Change.Id id, Throwable t) {
       String msg = "Error reading change";
-      log.error(msg + " " + id, t);
+      logger.atSevere().withCause(t).log("%s %s", msg, id);
       problems.put(id, msg);
     }
 
     public void logProblem(Change.Id id, String msg) {
-      log.error(msg + " " + id);
+      logger.atSevere().log("%s %s", msg, id);
       problems.put(id, msg);
     }
 
@@ -390,7 +389,7 @@
         commitStatus.problem(cd.getId(), e.getMessage());
       } catch (OrmException e) {
         String msg = "Error checking submit rules for change";
-        log.warn(msg + " " + cd.getId(), e);
+        logger.atWarning().withCause(e).log("%s %s", msg, cd.getId());
         commitStatus.problem(cd.getId(), msg);
       }
     }
@@ -444,7 +443,7 @@
     this.db = db;
     openRepoManager();
 
-    logDebug("Beginning integration of {}", change);
+    logDebug("Beginning integration of %s", change);
     try {
       ChangeSet indexBackedChangeSet =
           mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(db, change, caller);
@@ -457,7 +456,7 @@
         throw new AuthException(
             "A change to be submitted with " + change.getId() + " is not visible");
       }
-      logDebug("Calculated to merge {}", indexBackedChangeSet);
+      logDebug("Calculated to merge %s", indexBackedChangeSet);
 
       // Reload ChangeSet so that we don't rely on (potentially) stale index data for merging
       ChangeSet cs = reloadChanges(indexBackedChangeSet);
@@ -476,7 +475,7 @@
             long attempt = retryTracker.lastAttemptNumber + 1;
             boolean isRetry = attempt > 1;
             if (isRetry) {
-              logDebug("Retrying, attempt #{}; skipping merged changes", attempt);
+              logDebug("Retrying, attempt #%d; skipping merged changes", attempt);
               this.ts = TimeUtil.nowTs();
               openRepoManager();
             }
@@ -566,7 +565,7 @@
   private void integrateIntoHistory(ChangeSet cs)
       throws IntegrationException, RestApiException, UpdateException {
     checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
-    logDebug("Beginning merge attempt on {}", cs);
+    logDebug("Beginning merge attempt on %s", cs);
     Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
 
     ListMultimap<Branch.NameKey, ChangeData> cbb;
@@ -708,7 +707,7 @@
       throw new IntegrationException("Failed to determine already accepted commits.", e);
     }
 
-    logDebug("Found {} existing heads", alreadyAccepted.size());
+    logDebug("Found %d existing heads", alreadyAccepted.size());
     return alreadyAccepted;
   }
 
@@ -722,7 +721,7 @@
 
   private BranchBatch validateChangeList(OpenRepo or, Collection<ChangeData> submitted)
       throws IntegrationException {
-    logDebug("Validating {} changes", submitted.size());
+    logDebug("Validating %d changes", submitted.size());
     Set<CodeReviewCommit> toSubmit = new LinkedHashSet<>(submitted.size());
     SetMultimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted);
 
@@ -827,7 +826,7 @@
       commit.add(or.canMergeFlag);
       toSubmit.add(commit);
     }
-    logDebug("Submitting on this run: {}", toSubmit);
+    logDebug("Submitting on this run: %s", toSubmit);
     return new AutoValue_MergeOp_BranchBatch(submitType, toSubmit);
   }
 
@@ -937,32 +936,24 @@
         + " failed";
   }
 
-  private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(submissionId + msg, args);
-    }
+  private void logDebug(String msg) {
+    logger.atFine().log(submissionId + msg);
+  }
+
+  private void logDebug(String msg, @Nullable Object arg) {
+    logger.atFine().log(submissionId + msg, arg);
   }
 
   private void logWarn(String msg, Throwable t) {
-    if (log.isWarnEnabled()) {
-      log.warn(submissionId + msg, t);
-    }
+    logger.atWarning().withCause(t).log("%s%s", submissionId, msg);
   }
 
   private void logWarn(String msg) {
-    if (log.isWarnEnabled()) {
-      log.warn(submissionId + msg);
-    }
+    logger.atWarning().log("%s%s", submissionId, msg);
   }
 
   private void logError(String msg, Throwable t) {
-    if (log.isErrorEnabled()) {
-      if (t != null) {
-        log.error(submissionId + msg, t);
-      } else {
-        log.error(submissionId + msg);
-      }
-    }
+    logger.atSevere().withCause(t).log("%s%s", submissionId, msg);
   }
 
   private void logError(String msg) {
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index c11ce4f..7ec8b0e 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -32,11 +33,9 @@
 import java.util.Set;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class RebaseSorter {
-  private static final Logger log = LoggerFactory.getLogger(RebaseSorter.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
@@ -110,8 +109,8 @@
       // check if the commit is merged in other branches
       for (RevCommit accepted : alreadyAccepted) {
         if (mirw.isMergedInto(mirw.parseCommit(commit), mirw.parseCommit(accepted))) {
-          log.debug(
-              "Dependency {} merged into branch head {}.", commit.getName(), accepted.getName());
+          logger.atFine().log(
+              "Dependency %s merged into branch head %s.", commit.getName(), accepted.getName());
           return true;
         }
       }
@@ -121,8 +120,8 @@
       for (ChangeData change : changes) {
         if (change.change().getStatus() == Status.MERGED
             && change.change().getDest().equals(dest)) {
-          log.debug(
-              "Dependency {} associated with merged change {}.", commit.getName(), change.getId());
+          logger.atFine().log(
+              "Dependency %s associated with merged change %s.", commit.getName(), change.getId());
           return true;
         }
       }
diff --git a/java/com/google/gerrit/server/submit/SubmitDryRun.java b/java/com/google/gerrit/server/submit/SubmitDryRun.java
index a0b927a..055e3cc 100644
--- a/java/com/google/gerrit/server/submit/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/submit/SubmitDryRun.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -41,12 +42,10 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Dry run of a submit strategy. */
 public class SubmitDryRun {
-  private static final Logger log = LoggerFactory.getLogger(SubmitDryRun.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static class Arguments {
     final Repository repo;
@@ -135,7 +134,7 @@
       case INHERIT:
       default:
         String errorMsg = "No submit strategy for: " + submitType;
-        log.error(errorMsg);
+        logger.atSevere().log(errorMsg);
         throw new IntegrationException(errorMsg);
     }
   }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
index a2ddb16..2cb0744 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.submit;
 
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -32,13 +33,11 @@
 import java.util.Set;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Factory to create a {@link SubmitStrategy} for a {@link SubmitType}. */
 @Singleton
 public class SubmitStrategyFactory {
-  private static final Logger log = LoggerFactory.getLogger(SubmitStrategyFactory.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final SubmitStrategy.Arguments.Factory argsFactory;
 
@@ -97,7 +96,7 @@
       case INHERIT:
       default:
         String errorMsg = "No submit strategy for: " + submitType;
-        log.error(errorMsg);
+        logger.atSevere().log(errorMsg);
         throw new IntegrationException(errorMsg);
     }
   }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 0c6c10e..62dabae 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -21,6 +21,8 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -63,11 +65,9 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 abstract class SubmitStrategyOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(SubmitStrategyOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected final SubmitStrategy.Arguments args;
   protected final CodeReviewCommit toMerge;
@@ -103,7 +103,7 @@
 
   @Override
   public final void updateRepo(RepoContext ctx) throws Exception {
-    logDebug("{}#updateRepo for change {}", getClass().getSimpleName(), toMerge.change().getId());
+    logDebug("%s#updateRepo for change %s", getClass().getSimpleName(), toMerge.change().getId());
     checkState(
         ctx.getRevWalk() == args.rw,
         "SubmitStrategyOp requires callers to call BatchUpdate#setRepository with exactly the same"
@@ -117,7 +117,7 @@
     if (alreadyMergedCommit == null) {
       updateRepoImpl(ctx);
     } else {
-      logDebug("Already merged as {}", alreadyMergedCommit.name());
+      logDebug("Already merged as %s", alreadyMergedCommit.name());
     }
     CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip();
 
@@ -128,7 +128,7 @@
       logDebug("No merge tip, no update to perform");
       return;
     }
-    logDebug("Moved tip from {} to {}", tipBefore, tipAfter);
+    logDebug("Moved tip from %s to %s", tipBefore, tipAfter);
 
     checkProjectConfig(ctx, tipAfter);
 
@@ -144,7 +144,7 @@
       throws IntegrationException {
     String refName = getDest().get();
     if (RefNames.REFS_CONFIG.equals(refName)) {
-      logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
+      logDebug("Loading new configuration from %s", RefNames.REFS_CONFIG);
       try {
         ProjectConfig cfg = new ProjectConfig(getProject());
         cfg.load(ctx.getRevWalk(), commit);
@@ -216,7 +216,7 @@
 
   @Override
   public final boolean updateChange(ChangeContext ctx) throws Exception {
-    logDebug("{}#updateChange for change {}", getClass().getSimpleName(), toMerge.change().getId());
+    logDebug("%s#updateChange for change %s", getClass().getSimpleName(), toMerge.change().getId());
     toMerge.setNotes(ctx.getNotes()); // Update change and notes from ctx.
     PatchSet.Id oldPsId = checkNotNull(toMerge.getPatchsetId());
     PatchSet.Id newPsId;
@@ -227,7 +227,7 @@
       if (alreadyMergedCommit == null) {
         logDebug(
             "Change is already merged according to its status, but we were unable to find it"
-                + " merged into the current tip ({})",
+                + " merged into the current tip (%s)",
             args.mergeTip.getCurrentTip().name());
       } else {
         logDebug("Change is already merged");
@@ -276,7 +276,7 @@
     checkNotNull(commit, "missing commit for change " + id);
     CommitMergeStatus s = commit.getStatusCode();
     checkNotNull(s, "status not set for change " + id + " expected to previously fail fast");
-    logDebug("Status of change {} ({}) on {}: {}", id, commit.name(), c.getDest(), s);
+    logDebug("Status of change %s (%s) on %s: %s", id, commit.name(), c.getDest(), s);
     setApproval(ctx, args.caller);
 
     mergeResultRev =
@@ -290,7 +290,7 @@
       setMerged(ctx, message(ctx, commit, s));
     } catch (OrmException err) {
       String msg = "Error updating change status for " + id;
-      log.error(msg, err);
+      logger.atSevere().withCause(err).log(msg);
       args.commitStatus.logProblem(id, msg);
       // It's possible this happened before updating anything in the db, but
       // it's hard to know for sure, so just return true below to be safe.
@@ -302,7 +302,7 @@
   private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx)
       throws IOException, OrmException {
     PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
-    logDebug("Fixing up already-merged patch set {}", psId);
+    logDebug("Fixing up already-merged patch set %s", psId);
     PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
     ctx.getRevWalk().parseBody(alreadyMergedCommit);
     ctx.getChange()
@@ -336,7 +336,7 @@
     PatchSet.Id oldPsId = toMerge.getPatchsetId();
     PatchSet.Id newPsId = ctx.getChange().currentPatchSetId();
 
-    logDebug("Add approval for " + id);
+    logDebug("Add approval for %s", id);
     ChangeUpdate origPsUpdate = ctx.getUpdate(oldPsId);
     origPsUpdate.putReviewer(user.getAccountId(), REVIEWER);
     LabelNormalizer.Result normalized = approve(ctx, origPsUpdate);
@@ -402,7 +402,7 @@
     // change happened.
     for (PatchSetApproval psa : normalized.unchanged()) {
       if (includeUnchanged || psa.isLegacySubmit()) {
-        logDebug("Adding submit label " + psa);
+        logDebug("Adding submit label %s", psa);
         update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
       }
     }
@@ -493,7 +493,7 @@
   private void setMerged(ChangeContext ctx, ChangeMessage msg) throws OrmException {
     Change c = ctx.getChange();
     ReviewDb db = ctx.getDb();
-    logDebug("Setting change {} merged", c.getId());
+    logDebug("Setting change %s merged", c.getId());
     c.setStatus(Change.Status.MERGED);
     c.setSubmissionId(args.submissionId.toStringForStorage());
 
@@ -516,7 +516,7 @@
       // If we naively execute postUpdate even if the change is already merged when updateChange
       // being, then we are subject to a race where postUpdate steps are run twice if two submit
       // processes run at the same time.
-      logDebug("Skipping post-update steps for change {}", getId());
+      logDebug("Skipping post-update steps for change %s", getId());
       return;
     }
     postUpdateImpl(ctx);
@@ -532,7 +532,7 @@
         try (Repository git = args.repoManager.openRepository(getProject())) {
           git.setGitwebDescription(p.getProject().getDescription());
         } catch (IOException e) {
-          log.error("cannot update description of " + p.getName(), e);
+          logger.atSevere().withCause(e).log("cannot update description of %s", p.getName());
         }
       }
     }
@@ -549,7 +549,7 @@
               args.accountsToNotify)
           .sendAsync();
     } catch (Exception e) {
-      log.error("Cannot email merged notification for " + getId(), e);
+      logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
     }
     if (mergeResultRev != null && !args.dryrun) {
       args.changeMerged.fire(
@@ -601,26 +601,33 @@
     }
   }
 
-  protected final void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(this.args.submissionId + msg, args);
-    }
+  protected final void logDebug(String msg) {
+    logger.atFine().log(this.args.submissionId + msg);
+  }
+
+  protected final void logDebug(String msg, @Nullable Object arg) {
+    logger.atFine().log(this.args.submissionId + msg, arg);
+  }
+
+  protected final void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+    logger.atFine().log(this.args.submissionId + msg, arg1, arg2);
+  }
+
+  protected final void logDebug(
+      String msg,
+      @Nullable Object arg1,
+      @Nullable Object arg2,
+      @Nullable Object arg3,
+      @Nullable Object arg4) {
+    logger.atFine().log(this.args.submissionId + msg, arg1, arg2, arg3, arg4);
   }
 
   protected final void logWarn(String msg, Throwable t) {
-    if (log.isWarnEnabled()) {
-      log.warn(args.submissionId + msg, t);
-    }
+    logger.atWarning().withCause(t).log("%s%s", args.submissionId, msg);
   }
 
   protected void logError(String msg, Throwable t) {
-    if (log.isErrorEnabled()) {
-      if (t != null) {
-        log.error(args.submissionId + msg, t);
-      } else {
-        log.error(args.submissionId + msg);
-      }
-    }
+    logger.atSevere().withCause(t).log("%s%s", args.submissionId, msg);
   }
 
   protected void logError(String msg) {
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index 4b24275..7e9fa6a 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -19,6 +19,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -70,10 +72,9 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SubmoduleOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Only used for branches without code review changes */
   public class GitlinkOp implements RepoOnlyOp {
@@ -128,8 +129,6 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
-
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
   private final ProjectCache projectCache;
@@ -214,7 +213,7 @@
       LinkedHashSet<Branch.NameKey> currentVisited,
       LinkedHashSet<Branch.NameKey> allVisited)
       throws SubmoduleException {
-    logDebug("Now processing " + current);
+    logDebug("Now processing %s", current);
 
     if (currentVisited.contains(current)) {
       throw new SubmoduleException(
@@ -276,9 +275,9 @@
   private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src, SubscribeSection s)
       throws IOException {
     Collection<Branch.NameKey> ret = new HashSet<>();
-    logDebug("Inspecting SubscribeSection " + s);
+    logDebug("Inspecting SubscribeSection %s", s);
     for (RefSpec r : s.getMatchingRefSpecs()) {
-      logDebug("Inspecting [matching] ref " + r);
+      logDebug("Inspecting [matching] ref %s", r);
       if (!r.matchSource(src.get())) {
         continue;
       }
@@ -296,7 +295,7 @@
     }
 
     for (RefSpec r : s.getMultiMatchRefSpecs()) {
-      logDebug("Inspecting [all] ref " + r);
+      logDebug("Inspecting [all] ref %s", r);
       if (!r.matchSource(src.get())) {
         continue;
       }
@@ -320,17 +319,17 @@
         }
       }
     }
-    logDebug("Returning possible branches: " + ret + "for project " + s.getProject());
+    logDebug("Returning possible branches: %s for project %s", ret, s.getProject());
     return ret;
   }
 
   public Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
       Branch.NameKey srcBranch) throws IOException {
-    logDebug("Calculating possible superprojects for " + srcBranch);
+    logDebug("Calculating possible superprojects for %s", srcBranch);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
     Project.NameKey srcProject = srcBranch.getParentKey();
     for (SubscribeSection s : projectCache.get(srcProject).getSubscribeSections(srcBranch)) {
-      logDebug("Checking subscribe section " + s);
+      logDebug("Checking subscribe section %s", s);
       Collection<Branch.NameKey> branches = getDestinationBranches(srcBranch, s);
       for (Branch.NameKey targetBranch : branches) {
         Project.NameKey targetProject = targetBranch.getParentKey();
@@ -338,11 +337,11 @@
           OpenRepo or = orm.getRepo(targetProject);
           ObjectId id = or.repo.resolve(targetBranch.get());
           if (id == null) {
-            logDebug("The branch " + targetBranch + " doesn't exist.");
+            logDebug("The branch %s doesn't exist.", targetBranch);
             continue;
           }
         } catch (NoSuchProjectException e) {
-          logDebug("The project " + targetProject + " doesn't exist");
+          logDebug("The project %s doesn't exist", targetProject);
           continue;
         }
 
@@ -354,7 +353,7 @@
         ret.addAll(m.subscribedTo(srcBranch));
       }
     }
-    logDebug("Calculated superprojects for " + srcBranch + " are " + ret);
+    logDebug("Calculated superprojects for %s are %s", srcBranch, ret);
     return ret;
   }
 
@@ -678,9 +677,15 @@
     bu.addRepoOnlyOp(new GitlinkOp(branch));
   }
 
-  private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(orm.getSubmissionId() + msg, args);
-    }
+  private void logDebug(String msg) {
+    logger.atFine().log(orm.getSubmissionId() + " " + msg);
+  }
+
+  private void logDebug(String msg, @Nullable Object arg) {
+    logger.atFine().log(orm.getSubmissionId() + " " + msg, arg);
+  }
+
+  private void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+    logger.atFine().log(orm.getSubmissionId() + " " + msg, arg1, arg2);
   }
 }
diff --git a/java/com/google/gerrit/server/submit/TestHelperOp.java b/java/com/google/gerrit/server/submit/TestHelperOp.java
index b3a82de..2f0a3f6 100644
--- a/java/com/google/gerrit/server/submit/TestHelperOp.java
+++ b/java/com/google/gerrit/server/submit/TestHelperOp.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -22,11 +24,9 @@
 import java.io.IOException;
 import java.util.Queue;
 import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class TestHelperOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(TestHelperOp.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Change.Id changeId;
   private final TestSubmitInput input;
@@ -42,7 +42,7 @@
   public void updateRepo(RepoContext ctx) throws IOException {
     Queue<Boolean> q = input.generateLockFailures;
     if (q != null && !q.isEmpty() && q.remove()) {
-      logDebug("Adding bogus ref update to trigger lock failure, via change {}", changeId);
+      logDebug("Adding bogus ref update to trigger lock failure, via change %s", changeId);
       ctx.addRefUpdate(
           ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
           ObjectId.zeroId(),
@@ -50,9 +50,7 @@
     }
   }
 
-  private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(submissionId + msg, args);
-    }
+  private void logDebug(String msg, @Nullable Object arg) {
+    logger.atFine().log(submissionId + msg, arg);
   }
 }
diff --git a/java/com/google/gerrit/server/tools/ToolsCatalog.java b/java/com/google/gerrit/server/tools/ToolsCatalog.java
index b616791..aaa366c 100644
--- a/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
 import com.google.inject.Inject;
@@ -34,8 +35,6 @@
 import java.util.TreeMap;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Listing of all client side tools stored on this server.
@@ -45,7 +44,7 @@
  */
 @Singleton
 public class ToolsCatalog {
-  private static final Logger log = LoggerFactory.getLogger(ToolsCatalog.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final SortedMap<String, Entry> toc;
 
@@ -127,7 +126,7 @@
       }
       return out.toByteArray();
     } catch (Exception e) {
-      log.debug("Cannot read " + path, e);
+      logger.atFine().withCause(e).log("Cannot read %s", path);
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 549b134..dd3cc73 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -25,6 +25,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multiset;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -62,8 +63,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Helper for a set of updates that should be applied for a site.
@@ -88,7 +87,7 @@
  * successfully before proceeding to the next phase.
  */
 public abstract class BatchUpdate implements AutoCloseable {
-  private static final Logger log = LoggerFactory.getLogger(BatchUpdate.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static Module module() {
     return new FactoryModule() {
@@ -386,17 +385,38 @@
   }
 
   protected void logDebug(String msg, Throwable t) {
-    if (requestId != null && log.isDebugEnabled()) {
-      log.debug(requestId + msg, t);
-    }
-  }
-
-  protected void logDebug(String msg, Object... args) {
     // Only log if there is a requestId assigned, since those are the
     // expensive/complicated requests like MergeOp. Doing it every time would be
     // noisy.
-    if (requestId != null && log.isDebugEnabled()) {
-      log.debug(requestId + msg, args);
+    if (requestId != null) {
+      logger.atFine().withCause(t).log(requestId + "%s", msg);
+    }
+  }
+
+  protected void logDebug(String msg) {
+    // Only log if there is a requestId assigned, since those are the
+    // expensive/complicated requests like MergeOp. Doing it every time would be
+    // noisy.
+    if (requestId != null) {
+      logger.atFine().log(requestId + msg);
+    }
+  }
+
+  protected void logDebug(String msg, @Nullable Object arg) {
+    // Only log if there is a requestId assigned, since those are the
+    // expensive/complicated requests like MergeOp. Doing it every time would be
+    // noisy.
+    if (requestId != null) {
+      logger.atFine().log(requestId + msg, arg);
+    }
+  }
+
+  protected void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+    // Only log if there is a requestId assigned, since those are the
+    // expensive/complicated requests like MergeOp. Doing it every time would be
+    // noisy.
+    if (requestId != null) {
+      logger.atFine().log(requestId + msg, arg1, arg2);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
index c10ae1b..8612fac 100644
--- a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
@@ -303,13 +303,13 @@
 
   private void executeUpdateRepo() throws UpdateException, RestApiException {
     try {
-      logDebug("Executing updateRepo on {} ops", ops.size());
+      logDebug("Executing updateRepo on %d ops", ops.size());
       RepoContextImpl ctx = new RepoContextImpl();
       for (BatchUpdateOp op : ops.values()) {
         op.updateRepo(ctx);
       }
 
-      logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size());
+      logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
       for (RepoOnlyOp op : repoOnlyOps) {
         op.updateRepo(ctx);
       }
@@ -358,7 +358,7 @@
       if (dryrun) {
         return ImmutableList.of();
       }
-      logDebug("Reindexing {} changes", results.size());
+      logDebug("Reindexing %d changes", results.size());
       List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
           new ArrayList<>(results.size());
       for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
@@ -405,7 +405,7 @@
       Change.Id id = e.getKey();
       ChangeContextImpl ctx = newChangeContext(id);
       boolean dirty = false;
-      logDebug("Applying {} ops for change {}", e.getValue().size(), id);
+      logDebug("Applying %d ops for change %s", e.getValue().size(), id);
       for (BatchUpdateOp op : e.getValue()) {
         dirty |= op.updateChange(ctx);
       }
@@ -418,7 +418,7 @@
         handle.manager.add(u);
       }
       if (ctx.deleted) {
-        logDebug("Change {} was deleted", id);
+        logDebug("Change %s was deleted", id);
         handle.manager.deleteChange(id);
         handle.setResult(id, ChangeResult.DELETED);
       } else {
@@ -429,7 +429,7 @@
   }
 
   private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException {
-    logDebug("Opening change {} for update", id);
+    logDebug("Opening change %s for update", id);
     Change c = newChanges.get(id);
     boolean isNew = c != null;
     if (!isNew) {
@@ -438,7 +438,7 @@
       // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
       c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
     } else {
-      logDebug("Change {} is new", id);
+      logDebug("Change %s is new", id);
     }
     ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
     return new ChangeContextImpl(notes);
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 9cdb006..3c6f6fd 100644
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Stopwatch;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -82,8 +83,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * {@link BatchUpdate} implementation that supports mixed ReviewDb/NoteDb operations, depending on
@@ -102,7 +101,7 @@
  * attempt to reimplement this logic. Use {@code BatchUpdate} if at all possible.
  */
 public class ReviewDbBatchUpdate extends BatchUpdate {
-  private static final Logger log = LoggerFactory.getLogger(ReviewDbBatchUpdate.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface AssistedFactory {
     ReviewDbBatchUpdate create(
@@ -369,13 +368,13 @@
 
   private void executeUpdateRepo() throws UpdateException, RestApiException {
     try {
-      logDebug("Executing updateRepo on {} ops", ops.size());
+      logDebug("Executing updateRepo on %d ops", ops.size());
       RepoContextImpl ctx = new RepoContextImpl();
       for (BatchUpdateOp op : ops.values()) {
         op.updateRepo(ctx);
       }
 
-      logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size());
+      logDebug("Executing updateRepo on %d RepoOnlyOps", repoOnlyOps.size());
       for (RepoOnlyOp op : repoOnlyOps) {
         op.updateRepo(ctx);
       }
@@ -417,7 +416,7 @@
     if (user.isIdentifiedUser()) {
       batchRefUpdate.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
     }
-    logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size());
+    logDebug("Executing batch of %d ref updates", batchRefUpdate.getCommands().size());
     if (dryrun) {
       return;
     }
@@ -445,7 +444,7 @@
     boolean success = false;
     Stopwatch sw = Stopwatch.createStarted();
     try {
-      logDebug("Executing change ops (parallel? {})", parallel);
+      logDebug("Executing change ops (parallel? %s)", parallel);
       ListeningExecutorService executor =
           parallel ? changeUpdateExector : MoreExecutors.newDirectExecutorService();
 
@@ -469,13 +468,13 @@
               new ChangeTask(e.getKey(), e.getValue(), Thread.currentThread(), dryrun);
           tasks.add(task);
           if (!parallel) {
-            logDebug("Direct execution of task for ops: {}", ops);
+            logDebug("Direct execution of task for ops: %s", ops);
           }
           futures.add(executor.submit(task));
         }
         if (parallel) {
           logDebug(
-              "Waiting on futures for {} ops spanning {} changes", ops.size(), ops.keySet().size());
+              "Waiting on futures for %d ops spanning %d changes", ops.size(), ops.keySet().size());
         }
         Futures.allAsList(futures).get();
 
@@ -521,7 +520,7 @@
     //
     // See the comments in NoteDbUpdateManager#execute() for why we execute the
     // updates on the change repo first.
-    logDebug("Executing NoteDb updates for {} changes", tasks.size());
+    logDebug("Executing NoteDb updates for %d changes", tasks.size());
     try {
       initRepository();
       BatchRefUpdate changeRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate();
@@ -530,7 +529,7 @@
         int objs = 0;
         for (ChangeTask task : tasks) {
           if (task.noteDbResult == null) {
-            logDebug("No-op update to {}", task.id);
+            logDebug("No-op update to %s", task.id);
             continue;
           }
           for (ReceiveCommand cmd : task.noteDbResult.changeCommands()) {
@@ -543,9 +542,8 @@
           hasAllUsersCommands |= !task.noteDbResult.allUsersCommands().isEmpty();
         }
         logDebug(
-            "Collected {} objects and {} ref updates to change repo",
-            objs,
-            changeRefUpdate.getCommands().size());
+            "Collected %d objects and %d ref updates to change repo",
+            objs, changeRefUpdate.getCommands().size());
         executeNoteDbUpdate(getRevWalk(), ins, changeRefUpdate);
       }
 
@@ -564,9 +562,8 @@
             }
           }
           logDebug(
-              "Collected {} objects and {} ref updates to All-Users",
-              objs,
-              allUsersRefUpdate.getCommands().size());
+              "Collected %d objects and %d ref updates to All-Users",
+              objs, allUsersRefUpdate.getCommands().size());
           executeNoteDbUpdate(allUsersRw, allUsersIns, allUsersRefUpdate);
         }
       } else {
@@ -579,7 +576,7 @@
         // rebuilt the next time it is needed.
         //
         // Always log even without RequestId.
-        log.debug("Ignoring NoteDb update error after ReviewDb write", e);
+        logger.atFine().withCause(e).log("Ignoring NoteDb update error after ReviewDb write");
 
         // Otherwise, we can't prove it's safe to ignore the error, either because some change had
         // NOTE_DB primary, or a task failed before determining the primary storage.
@@ -670,7 +667,7 @@
           }
 
           // Call updateChange on each op.
-          logDebug("Calling updateChange on {} ops", changeOps.size());
+          logDebug("Calling updateChange on %s ops", changeOps.size());
           for (BatchUpdateOp op : changeOps) {
             dirty |= op.updateChange(ctx);
           }
@@ -708,7 +705,7 @@
               db.commit();
             }
           } else {
-            logDebug("Skipping ReviewDb write since primary storage is {}", storage);
+            logDebug("Skipping ReviewDb write since primary storage is %s", storage);
           }
         } finally {
           db.rollback();
@@ -732,7 +729,7 @@
             // already written the NoteDbChangeState to ReviewDb, which means
             // if the state is out of date it will be rebuilt the next time it
             // is needed.
-            log.debug("Ignoring NoteDb update error after ReviewDb write", ex);
+            logger.atFine().withCause(ex).log("Ignoring NoteDb update error after ReviewDb write");
           }
         }
       } catch (Exception e) {
@@ -827,15 +824,11 @@
     }
 
     private void logDebug(String msg, Throwable t) {
-      if (log.isDebugEnabled()) {
-        ReviewDbBatchUpdate.this.logDebug("[" + taskId + "]" + msg, t);
-      }
+      ReviewDbBatchUpdate.this.logDebug("[" + taskId + "] " + msg, t);
     }
 
     private void logDebug(String msg, Object... args) {
-      if (log.isDebugEnabled()) {
-        ReviewDbBatchUpdate.this.logDebug("[" + taskId + "]" + msg, args);
-      }
+      ReviewDbBatchUpdate.this.logDebug("[" + taskId + "] " + msg, args);
     }
   }
 
diff --git a/java/com/google/gerrit/server/util/MagicBranch.java b/java/com/google/gerrit/server/util/MagicBranch.java
index e757d77..e7d00f0 100644
--- a/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/java/com/google/gerrit/server/util/MagicBranch.java
@@ -14,17 +14,16 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.reviewdb.client.Project;
 import java.io.IOException;
 import java.util.Map;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public final class MagicBranch {
-  private static final Logger log = LoggerFactory.getLogger(MagicBranch.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String NEW_CHANGE = "refs/for/";
   // TODO(xchangcheng): remove after 'repo' supports private/wip changes.
@@ -95,16 +94,14 @@
       blockingFors = repo.getRefDatabase().getRefs(branchName);
     } catch (IOException err) {
       String projName = project.getName();
-      log.warn("Cannot scan refs in '" + projName + "'", err);
+      logger.atWarning().withCause(err).log("Cannot scan refs in '%s'", projName);
       return new Capable("Server process cannot read '" + projName + "'");
     }
     if (!blockingFors.isEmpty()) {
       String projName = project.getName();
-      log.error(
-          "Repository '"
-              + projName
-              + "' needs the following refs removed to receive changes: "
-              + blockingFors.keySet());
+      logger.atSevere().log(
+          "Repository '%s' needs the following refs removed to receive changes: %s",
+          projName, blockingFors.keySet());
       return new Capable("One or more " + branchName + " names blocks change upload");
     }
 
diff --git a/java/com/google/gerrit/server/util/SystemLog.java b/java/com/google/gerrit/server/util/SystemLog.java
index e1a0317..224a6d9 100644
--- a/java/com/google/gerrit/server/util/SystemLog.java
+++ b/java/com/google/gerrit/server/util/SystemLog.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -35,11 +36,10 @@
 import org.apache.log4j.spi.ErrorHandler;
 import org.apache.log4j.spi.LoggingEvent;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class SystemLog {
-  private static final org.slf4j.Logger log = LoggerFactory.getLogger(SystemLog.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static final String LOG4J_CONFIGURATION = "log4j.configuration";
 
@@ -90,8 +90,8 @@
       if (appender != null) {
         async.addAppender(appender);
       } else {
-        log.warn(
-            "No appender with the name: " + name + " was found. " + name + " logging is disabled");
+        logger.atWarning().log(
+            "No appender with the name: %s was found. %s logging is disabled", name, name);
       }
     }
     async.activateOptions();
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index c36d68b..6c810a3 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -29,12 +29,12 @@
         "//lib/bouncycastle:bcprov-neverlink",
         "//lib/commons:codec",
         "//lib/dropwizard:dropwizard-core",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/guice:guice-servlet",  # SSH should not depend on servlet
         "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/log:log4j",
         "//lib/mina:core",
         "//lib/mina:sshd",
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 3da8b5c..dae9016 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
@@ -59,11 +60,10 @@
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public abstract class BaseCommand implements Command {
-  private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static final Charset ENC = UTF_8;
 
   private static final int PRIVATE_STATUS = 1 << 30;
@@ -351,7 +351,7 @@
       }
       m.append(" during ");
       m.append(context.getCommandLine());
-      log.error(m.toString(), e);
+      logger.atSevere().withCause(e).log(m.toString());
     }
 
     if (e instanceof Failure) {
@@ -362,7 +362,7 @@
       } catch (IOException e2) {
         // Ignored
       } catch (Throwable e2) {
-        log.warn("Cannot send failure message to client", e2);
+        logger.atWarning().withCause(e2).log("Cannot send failure message to client");
       }
       return f.exitCode;
     }
@@ -373,7 +373,7 @@
     } catch (IOException e2) {
       // Ignored
     } catch (Throwable e2) {
-      log.warn("Cannot send internal server error message to client", e2);
+      logger.atWarning().withCause(e2).log("Cannot send internal server error message to client");
     }
     return 128;
   }
diff --git a/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
index 59185bf..13ca52e 100644
--- a/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
+++ b/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -44,9 +44,10 @@
     }
     int interactiveThreads = Math.max(1, poolSize - batchThreads);
     interactiveExecutor =
-        queues.createQueue(interactiveThreads, "SSH-Interactive-Worker", Thread.MIN_PRIORITY);
+        queues.createQueue(interactiveThreads, "SSH-Interactive-Worker", Thread.MIN_PRIORITY, true);
     if (batchThreads != 0) {
-      batchExecutor = queues.createQueue(batchThreads, "SSH-Batch-Worker", Thread.MIN_PRIORITY);
+      batchExecutor =
+          queues.createQueue(batchThreads, "SSH-Batch-Worker", Thread.MIN_PRIORITY, true);
     } else {
       batchExecutor = interactiveExecutor;
     }
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 4cd7487..3eef4d6 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Atomics;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -44,13 +45,11 @@
 import org.apache.sshd.server.SessionAware;
 import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Creates a CommandFactory using commands registered by {@link CommandModule}. */
 @Singleton
 class CommandFactoryProvider implements Provider<CommandFactory>, LifecycleListener {
-  private static final Logger logger = LoggerFactory.getLogger(CommandFactoryProvider.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DispatchCommandProvider dispatcher;
   private final SshLog log;
@@ -76,7 +75,7 @@
     createCommandInterceptor = i;
 
     int threads = cfg.getInt("sshd", "commandStartThreads", 2);
-    startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+    startExecutor = workQueue.createQueue(threads, "SshCommandStart", true);
     destroyExecutor =
         Executors.newSingleThreadExecutor(
             new ThreadFactoryBuilder()
@@ -166,12 +165,9 @@
                   try {
                     onStart();
                   } catch (Exception e) {
-                    logger.warn(
-                        "Cannot start command \""
-                            + ctx.getCommandLine()
-                            + "\" for user "
-                            + ctx.getSession().getUsername(),
-                        e);
+                    logger.atWarning().withCause(e).log(
+                        "Cannot start command \"%s\" for user %s",
+                        ctx.getCommandLine(), ctx.getSession().getUsername());
                   }
                 }
 
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 74cdd99..be17219 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Preconditions;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -44,12 +45,10 @@
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Authenticates by public key through {@link AccountSshKey} entities. */
 class DatabasePubKeyAuth implements PublickeyAuthenticator {
-  private static final Logger log = LoggerFactory.getLogger(DatabasePubKeyAuth.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final SshKeyCacheImpl sshKeyCache;
   private final SshLog sshLog;
@@ -203,13 +202,13 @@
       } catch (NoSuchFileException noFile) {
         return Collections.emptySet();
       } catch (IOException err) {
-        log.error("Cannot read " + path, err);
+        logger.atSevere().withCause(err).log("Cannot read %s", path);
         return Collections.emptySet();
       }
     }
 
     private static void logBadKey(Path path, String line, Exception e) {
-      log.warn("Invalid key in " + path + ":\n  " + line, e);
+      logger.atWarning().withCause(e).log("Invalid key in %s:\n  %s", path, line);
     }
 
     boolean isCurrent() {
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index ecd9476..688c573 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.metrics.Counter0;
@@ -70,7 +71,7 @@
 import org.apache.sshd.common.compression.BuiltinCompressions;
 import org.apache.sshd.common.compression.Compression;
 import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.forward.DefaultTcpipForwarderFactory;
+import org.apache.sshd.common.forward.DefaultForwarderFactory;
 import org.apache.sshd.common.future.CloseFuture;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.AbstractIoServiceFactory;
@@ -111,8 +112,6 @@
 import org.bouncycastle.crypto.prng.RandomGenerator;
 import org.bouncycastle.crypto.prng.VMPCRandomGenerator;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * SSH daemon to communicate with Gerrit.
@@ -133,7 +132,7 @@
  */
 @Singleton
 public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
-  private static final Logger sshDaemonLog = LoggerFactory.getLogger(SshDaemon.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public enum SshSessionBackend {
     MINA,
@@ -334,7 +333,7 @@
         throw new IllegalStateException("Cannot bind to " + addressList(), e);
       }
 
-      sshDaemonLog.info(String.format("Started Gerrit %s on %s", getVersion(), addressList()));
+      logger.atInfo().log("Started Gerrit %s on %s", getVersion(), addressList());
     }
   }
 
@@ -348,9 +347,9 @@
       try {
         daemonAcceptor.close(true).await();
         shutdownExecutors();
-        sshDaemonLog.info("Stopped Gerrit SSHD");
+        logger.atInfo().log("Stopped Gerrit SSHD");
       } catch (IOException e) {
-        sshDaemonLog.warn("Exception caught while closing", e);
+        logger.atWarning().withCause(e).log("Exception caught while closing");
       } finally {
         daemonAcceptor = null;
       }
@@ -400,9 +399,8 @@
         try {
           r.add(new HostKey(addr, keyBin));
         } catch (JSchException e) {
-          sshDaemonLog.warn(
-              String.format(
-                  "Cannot format SSHD host key [%s]: %s", pub.getAlgorithm(), e.getMessage()));
+          logger.atWarning().log(
+              "Cannot format SSHD host key [%s]: %s", pub.getAlgorithm(), e.getMessage());
         }
       }
     }
@@ -533,15 +531,12 @@
         final byte[] iv = new byte[c.getIVSize()];
         c.init(Cipher.Mode.Encrypt, key, iv);
       } catch (InvalidKeyException e) {
-        sshDaemonLog.warn(
-            "Disabling cipher "
-                + f.getName()
-                + ": "
-                + e.getMessage()
-                + "; try installing unlimited cryptography extension");
+        logger.atWarning().log(
+            "Disabling cipher %s: %s; try installing unlimited cryptography extension",
+            f.getName(), e.getMessage());
         i.remove();
       } catch (Exception e) {
-        sshDaemonLog.warn("Disabling cipher " + f.getName() + ": " + e.getMessage());
+        logger.atWarning().log("Disabling cipher %s: %s", f.getName(), e.getMessage());
         i.remove();
       }
     }
@@ -602,7 +597,7 @@
           msg.append(avail[i].getName());
         }
         msg.append(" is supported");
-        sshDaemonLog.error(msg.toString());
+        logger.atSevere().log(msg.toString());
       } else if (add) {
         if (!def.contains(n)) {
           def.add(n);
@@ -670,12 +665,11 @@
     List<NamedFactory<UserAuth>> authFactories = new ArrayList<>();
     if (kerberosKeytab != null) {
       authFactories.add(UserAuthGSSFactory.INSTANCE);
-      sshDaemonLog.info("Enabling kerberos with keytab " + kerberosKeytab);
+      logger.atInfo().log("Enabling kerberos with keytab %s", kerberosKeytab);
       if (!new File(kerberosKeytab).canRead()) {
-        sshDaemonLog.error(
-            "Keytab "
-                + kerberosKeytab
-                + " does not exist or is not readable; further errors are possible");
+        logger.atSevere().log(
+            "Keytab %s does not exist or is not readable; further errors are possible",
+            kerberosKeytab);
       }
       kerberosAuthenticator.setKeytabFile(kerberosKeytab);
       if (kerberosPrincipal == null) {
@@ -685,9 +679,9 @@
           kerberosPrincipal = "host/localhost";
         }
       }
-      sshDaemonLog.info("Using kerberos principal " + kerberosPrincipal);
+      logger.atInfo().log("Using kerberos principal %s", kerberosPrincipal);
       if (!kerberosPrincipal.startsWith("host/")) {
-        sshDaemonLog.warn(
+        logger.atWarning().log(
             "Host principal does not start with host/ "
                 + "which most SSH clients will supply automatically");
       }
@@ -700,7 +694,7 @@
   }
 
   private void initForwarding() {
-    setTcpipForwardingFilter(
+    setForwardingFilter(
         new ForwardingFilter() {
           @Override
           public boolean canForwardAgent(Session session, String requestType) {
@@ -722,7 +716,7 @@
             return false;
           }
         });
-    setTcpipForwarderFactory(new DefaultTcpipForwarderFactory());
+    setForwarderFactory(new DefaultForwarderFactory());
   }
 
   private void initFileSystemFactory() {
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 3ab7a58..b573062 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -18,6 +18,7 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -38,13 +39,12 @@
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Provides the {@link SshKeyCacheEntry}. */
 @Singleton
 public class SshKeyCacheImpl implements SshKeyCache {
-  private static final Logger log = LoggerFactory.getLogger(SshKeyCacheImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String CACHE_NAME = "sshkeys";
 
   static final Iterable<SshKeyCacheEntry> NO_SUCH_USER = none();
@@ -78,7 +78,7 @@
     try {
       return cache.get(username);
     } catch (ExecutionException e) {
-      log.warn("Cannot load SSH keys for " + username, e);
+      logger.atWarning().withCause(e).log("Cannot load SSH keys for %s", username);
       return Collections.emptyList();
     }
   }
@@ -135,11 +135,11 @@
 
     private void markInvalid(AccountSshKey k) {
       try {
-        log.info("Flagging SSH key " + k.seq() + " of account " + k.accountId() + " invalid");
+        logger.atInfo().log("Flagging SSH key %d of account %s invalid", k.seq(), k.accountId());
         authorizedKeys.markKeyInvalid(k.accountId(), k.seq());
       } catch (IOException | ConfigInvalidException e) {
-        log.error(
-            "Failed to mark SSH key " + k.seq() + " of account " + k.accountId() + " invalid", e);
+        logger.atSevere().withCause(e).log(
+            "Failed to mark SSH key %d of account %s invalid", k.seq(), k.accountId());
       }
     }
   }
diff --git a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
index bb47e3f..d89f9e0 100644
--- a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountSshKey;
@@ -21,11 +22,9 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.security.spec.InvalidKeySpecException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SshKeyCreatorImpl implements SshKeyCreator {
-  private static final Logger log = LoggerFactory.getLogger(SshKeyCreatorImpl.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   public AccountSshKey create(Account.Id accountId, int seq, String encoded)
@@ -38,7 +37,7 @@
       throw new InvalidSshKeyException();
 
     } catch (NoSuchProviderException e) {
-      log.error("Cannot parse SSH key", e);
+      logger.atSevere().withCause(e).log("Cannot parse SSH key");
       throw new InvalidSshKeyException();
     }
   }
diff --git a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 9ae1814..e9a095f 100644
--- a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.plugins.Plugin;
@@ -24,12 +25,10 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import org.apache.sshd.server.Command;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class SshPluginStarterCallback implements StartPluginListener, ReloadPluginListener {
-  private static final Logger log = LoggerFactory.getLogger(SshPluginStarterCallback.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final DispatchCommandProvider root;
   private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
@@ -65,11 +64,9 @@
         return plugin.getSshInjector().getProvider(key);
       } catch (RuntimeException err) {
         if (!providesDynamicOptions(plugin)) {
-          log.warn(
-              String.format(
-                  "Plugin %s did not define its top-level command nor any DynamicOptions",
-                  plugin.getName()),
-              err);
+          logger.atWarning().withCause(err).log(
+              "Plugin %s did not define its top-level command nor any DynamicOptions",
+              plugin.getName());
         }
       }
     }
diff --git a/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java b/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
index c3c6306..235da5d 100644
--- a/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
+++ b/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
@@ -34,6 +34,6 @@
 
   @Override
   public ScheduledThreadPoolExecutor get() {
-    return queues.createQueue(poolSize, "SSH-Stream-Worker", Thread.MIN_PRIORITY);
+    return queues.createQueue(poolSize, "SSH-Stream-Worker", Thread.MIN_PRIORITY, true);
   }
 }
diff --git a/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index ef66990..67ed098 100644
--- a/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.ProjectInfo;
@@ -42,46 +43,39 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
-  name = "set-project-parent",
-  description = "Change the project permissions are inherited from"
-)
+    name = "set-project-parent",
+    description = "Change the project permissions are inherited from")
 final class AdminSetParent extends SshCommand {
-  private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Option(
-    name = "--parent",
-    aliases = {"-p"},
-    metaVar = "NAME",
-    usage = "new parent project"
-  )
+      name = "--parent",
+      aliases = {"-p"},
+      metaVar = "NAME",
+      usage = "new parent project")
   private ProjectState newParent;
 
   @Option(
-    name = "--children-of",
-    metaVar = "NAME",
-    usage = "parent project for which the child projects should be reparented"
-  )
+      name = "--children-of",
+      metaVar = "NAME",
+      usage = "parent project for which the child projects should be reparented")
   private ProjectState oldParent;
 
   @Option(
-    name = "--exclude",
-    metaVar = "NAME",
-    usage = "child project of old parent project which should not be reparented"
-  )
+      name = "--exclude",
+      metaVar = "NAME",
+      usage = "child project of old parent project which should not be reparented")
   private List<ProjectState> excludedChildren = new ArrayList<>();
 
   @Argument(
-    index = 0,
-    required = false,
-    multiValued = true,
-    metaVar = "NAME",
-    usage = "projects to modify"
-  )
+      index = 0,
+      required = false,
+      multiValued = true,
+      metaVar = "NAME",
+      usage = "projects to modify")
   private List<ProjectState> children = new ArrayList<>();
 
   @Inject private ProjectCache projectCache;
@@ -172,7 +166,7 @@
         err.append("error: Project ").append(name).append(" not found\n");
       } catch (IOException | ConfigInvalidException e) {
         final String msg = "Cannot update project " + name;
-        log.error(msg, e);
+        logger.atSevere().withCause(e).log(msg);
         err.append("error: ").append(msg).append("\n");
       }
 
@@ -180,7 +174,7 @@
         projectCache.evict(nameKey);
       } catch (IOException e) {
         final String msg = "Cannot reindex project: " + name;
-        log.error(msg, e);
+        logger.atSevere().withCause(e).log(msg);
         err.append("error: ").append(msg).append("\n");
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/AproposCommand.java b/java/com/google/gerrit/sshd/commands/AproposCommand.java
index 577b58f..d3db70d 100644
--- a/java/com/google/gerrit/sshd/commands/AproposCommand.java
+++ b/java/com/google/gerrit/sshd/commands/AproposCommand.java
@@ -27,10 +27,9 @@
 import org.kohsuke.args4j.Argument;
 
 @CommandMetaData(
-  name = "apropos",
-  description = "Search in Gerrit documentation",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "apropos",
+    description = "Search in Gerrit documentation",
+    runsAt = MASTER_OR_SLAVE)
 final class AproposCommand extends SshCommand {
   @Inject private QueryDocumentationExecutor searcher;
   @Inject @CanonicalWebUrl String url;
diff --git a/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index cceb16b..415ac4c 100644
--- a/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -33,34 +33,30 @@
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(
-  name = "ban-commit",
-  description = "Ban a commit from a project's repository",
-  runsAt = MASTER
-)
+    name = "ban-commit",
+    description = "Ban a commit from a project's repository",
+    runsAt = MASTER)
 public class BanCommitCommand extends SshCommand {
   @Option(
-    name = "--reason",
-    aliases = {"-r"},
-    metaVar = "REASON",
-    usage = "reason for banning the commit"
-  )
+      name = "--reason",
+      aliases = {"-r"},
+      metaVar = "REASON",
+      usage = "reason for banning the commit")
   private String reason;
 
   @Argument(
-    index = 0,
-    required = true,
-    metaVar = "PROJECT",
-    usage = "name of the project for which the commit should be banned"
-  )
+      index = 0,
+      required = true,
+      metaVar = "PROJECT",
+      usage = "name of the project for which the commit should be banned")
   private ProjectState projectState;
 
   @Argument(
-    index = 1,
-    required = true,
-    multiValued = true,
-    metaVar = "COMMIT",
-    usage = "commit(s) that should be banned"
-  )
+      index = 1,
+      required = true,
+      multiValued = true,
+      metaVar = "COMMIT",
+      usage = "commit(s) that should be banned")
   private List<ObjectId> commitsToBan = new ArrayList<>();
 
   @Inject private BanCommit banCommit;
diff --git a/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
index 6703462..affb919 100644
--- a/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
+++ b/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
@@ -42,17 +42,15 @@
   protected String changeId;
 
   @Option(
-    name = "-s",
-    usage =
-        "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch"
-  )
+      name = "-s",
+      usage =
+          "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch")
   protected boolean useStdin;
 
   @Option(
-    name = "--no-filters",
-    aliases = {"-n"},
-    usage = "Don't run the submit_filter/2 from the parent projects"
-  )
+      name = "--no-filters",
+      aliases = {"-n"},
+      usage = "Don't run the submit_filter/2 from the parent projects")
   void setNoFilters(boolean no) {
     input.filters = no ? Filters.SKIP : Filters.RUN;
   }
diff --git a/java/com/google/gerrit/sshd/commands/CloseConnection.java b/java/com/google/gerrit/sshd/commands/CloseConnection.java
index 0e101a9..a38461d 100644
--- a/java/com/google/gerrit/sshd/commands/CloseConnection.java
+++ b/java/com/google/gerrit/sshd/commands/CloseConnection.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
@@ -33,30 +34,25 @@
 import org.apache.sshd.common.session.helpers.AbstractSession;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Close specified SSH connections */
 @AdminHighPriorityCommand
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
-  name = "close-connection",
-  description = "Close the specified SSH connection",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "close-connection",
+    description = "Close the specified SSH connection",
+    runsAt = MASTER_OR_SLAVE)
 final class CloseConnection extends SshCommand {
-
-  private static final Logger log = LoggerFactory.getLogger(CloseConnection.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject private SshDaemon sshDaemon;
 
   @Argument(
-    index = 0,
-    multiValued = true,
-    required = true,
-    metaVar = "SESSION_ID",
-    usage = "List of SSH session IDs to be closed"
-  )
+      index = 0,
+      multiValued = true,
+      required = true,
+      metaVar = "SESSION_ID",
+      usage = "List of SSH session IDs to be closed")
   private final List<String> sessionIds = new ArrayList<>();
 
   @Option(name = "--wait", usage = "wait for connection to close before exiting")
@@ -84,7 +80,8 @@
               future.await();
               stdout.println("closed connection " + sessionId);
             } catch (IOException e) {
-              log.warn("Wait for connection to close interrupted: " + e.getMessage());
+              logger.atWarning().log(
+                  "Wait for connection to close interrupted: %s", e.getMessage());
             }
           }
           break;
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 32bff8c..9dc9a50 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -42,11 +42,10 @@
 @CommandMetaData(name = "create-account", description = "Create a new batch/role account")
 final class CreateAccountCommand extends SshCommand {
   @Option(
-    name = "--group",
-    aliases = {"-g"},
-    metaVar = "GROUP",
-    usage = "groups to add account to"
-  )
+      name = "--group",
+      aliases = {"-g"},
+      metaVar = "GROUP",
+      usage = "groups to add account to")
   private List<AccountGroup.Id> groups = new ArrayList<>();
 
   @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
@@ -59,10 +58,9 @@
   private String sshKey;
 
   @Option(
-    name = "--http-password",
-    metaVar = "PASSWORD",
-    usage = "password for HTTP authentication"
-  )
+      name = "--http-password",
+      metaVar = "PASSWORD",
+      usage = "password for HTTP authentication")
   private String httpPassword;
 
   @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "name of the user account")
diff --git a/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java b/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
index fd1e189..aad96a1 100644
--- a/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
@@ -34,11 +34,10 @@
   private String name;
 
   @Argument(
-    index = 2,
-    required = true,
-    metaVar = "REVISION",
-    usage = "base revision of the new branch"
-  )
+      index = 2,
+      required = true,
+      metaVar = "REVISION",
+      usage = "base revision of the new branch")
   private String revision;
 
   @Inject GerritApi gApi;
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 1e1e254..5a83b01 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -50,19 +50,17 @@
 @CommandMetaData(name = "create-group", description = "Create a new account group")
 final class CreateGroupCommand extends SshCommand {
   @Option(
-    name = "--owner",
-    aliases = {"-o"},
-    metaVar = "GROUP",
-    usage = "owning group, if not specified the group will be self-owning"
-  )
+      name = "--owner",
+      aliases = {"-o"},
+      metaVar = "GROUP",
+      usage = "owning group, if not specified the group will be self-owning")
   private AccountGroup.Id ownerGroupId;
 
   @Option(
-    name = "--description",
-    aliases = {"-d"},
-    metaVar = "DESC",
-    usage = "description of group"
-  )
+      name = "--description",
+      aliases = {"-d"},
+      metaVar = "DESC",
+      usage = "description of group")
   private String groupDescription = "";
 
   @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of group to be created")
@@ -71,11 +69,10 @@
   private final Set<Account.Id> initialMembers = new HashSet<>();
 
   @Option(
-    name = "--member",
-    aliases = {"-m"},
-    metaVar = "USERNAME",
-    usage = "initial set of users to become members of the group"
-  )
+      name = "--member",
+      aliases = {"-m"},
+      metaVar = "USERNAME",
+      usage = "initial set of users to become members of the group")
   void addMember(Account.Id id) {
     initialMembers.add(id);
   }
@@ -86,11 +83,10 @@
   private final Set<AccountGroup.UUID> initialGroups = new HashSet<>();
 
   @Option(
-    name = "--group",
-    aliases = "-g",
-    metaVar = "GROUP",
-    usage = "initial set of groups to be included in the group"
-  )
+      name = "--group",
+      aliases = "-g",
+      metaVar = "GROUP",
+      usage = "initial set of groups to be included in the group")
   void addGroup(AccountGroup.UUID id) {
     initialGroups.add(id);
   }
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index fa4a573..df86d63 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -42,50 +42,44 @@
 /** Create a new project. * */
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 @CommandMetaData(
-  name = "create-project",
-  description = "Create a new project and associated Git repository"
-)
+    name = "create-project",
+    description = "Create a new project and associated Git repository")
 final class CreateProjectCommand extends SshCommand {
   @Option(
-    name = "--suggest-parents",
-    aliases = {"-S"},
-    usage =
-        "suggest parent candidates, "
-            + "if this option is used all other options and arguments are ignored"
-  )
+      name = "--suggest-parents",
+      aliases = {"-S"},
+      usage =
+          "suggest parent candidates, "
+              + "if this option is used all other options and arguments are ignored")
   private boolean suggestParent;
 
   @Option(
-    name = "--owner",
-    aliases = {"-o"},
-    usage = "owner(s) of project"
-  )
+      name = "--owner",
+      aliases = {"-o"},
+      usage = "owner(s) of project")
   private List<AccountGroup.UUID> ownerIds;
 
   @Option(
-    name = "--parent",
-    aliases = {"-p"},
-    metaVar = "NAME",
-    usage = "parent project"
-  )
+      name = "--parent",
+      aliases = {"-p"},
+      metaVar = "NAME",
+      usage = "parent project")
   private ProjectState newParent;
 
   @Option(name = "--permissions-only", usage = "create project for use only as parent")
   private boolean permissionsOnly;
 
   @Option(
-    name = "--description",
-    aliases = {"-d"},
-    metaVar = "DESCRIPTION",
-    usage = "description of project"
-  )
+      name = "--description",
+      aliases = {"-d"},
+      metaVar = "DESCRIPTION",
+      usage = "description of project")
   private String projectDescription = "";
 
   @Option(
-    name = "--submit-type",
-    aliases = {"-t"},
-    usage = "project submit type"
-  )
+      name = "--submit-type",
+      aliases = {"-t"},
+      usage = "project submit type")
   private SubmitType submitType;
 
   @Option(name = "--contributor-agreements", usage = "if contributor agreement is required")
@@ -104,25 +98,22 @@
   private InheritableBoolean rejectEmptyCommit = InheritableBoolean.INHERIT;
 
   @Option(
-    name = "--new-change-for-all-not-in-target",
-    usage = "if a new change will be created for every commit not in target branch"
-  )
+      name = "--new-change-for-all-not-in-target",
+      usage = "if a new change will be created for every commit not in target branch")
   private InheritableBoolean createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
 
   @Option(
-    name = "--use-contributor-agreements",
-    aliases = {"--ca"},
-    usage = "if contributor agreement is required"
-  )
+      name = "--use-contributor-agreements",
+      aliases = {"--ca"},
+      usage = "if contributor agreement is required")
   void setUseContributorArgreements(@SuppressWarnings("unused") boolean on) {
     contributorAgreements = InheritableBoolean.TRUE;
   }
 
   @Option(
-    name = "--use-signed-off-by",
-    aliases = {"--so"},
-    usage = "if signed-off-by is required"
-  )
+      name = "--use-signed-off-by",
+      aliases = {"--so"},
+      usage = "if signed-off-by is required")
   void setUseSignedOffBy(@SuppressWarnings("unused") boolean on) {
     signedOffBy = InheritableBoolean.TRUE;
   }
@@ -133,29 +124,26 @@
   }
 
   @Option(
-    name = "--require-change-id",
-    aliases = {"--id"},
-    usage = "if change-id is required"
-  )
+      name = "--require-change-id",
+      aliases = {"--id"},
+      usage = "if change-id is required")
   void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
     requireChangeID = InheritableBoolean.TRUE;
   }
 
   @Option(
-    name = "--create-new-change-for-all-not-in-target",
-    aliases = {"--ncfa"},
-    usage = "if a new change will be created for every commit not in target branch"
-  )
+      name = "--create-new-change-for-all-not-in-target",
+      aliases = {"--ncfa"},
+      usage = "if a new change will be created for every commit not in target branch")
   void setNewChangeForAllNotInTarget(@SuppressWarnings("unused") boolean on) {
     createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
   }
 
   @Option(
-    name = "--branch",
-    aliases = {"-b"},
-    metaVar = "BRANCH",
-    usage = "initial branch name\n(default: master)"
-  )
+      name = "--branch",
+      aliases = {"-b"},
+      metaVar = "BRANCH",
+      usage = "initial branch name\n(default: master)")
   private List<String> branch;
 
   @Option(name = "--empty-commit", usage = "to create initial empty commit")
@@ -165,9 +153,8 @@
   private String maxObjectSizeLimit;
 
   @Option(
-    name = "--plugin-config",
-    usage = "plugin configuration parameter with format '<plugin-name>.<parameter-name>=<value>'"
-  )
+      name = "--plugin-config",
+      usage = "plugin configuration parameter with format '<plugin-name>.<parameter-name>=<value>'")
   private List<String> pluginConfigValues;
 
   @Argument(index = 0, metaVar = "NAME", usage = "name of project to be created")
diff --git a/java/com/google/gerrit/sshd/commands/FlushCaches.java b/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 2271ece..df56cf4 100644
--- a/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -37,10 +37,9 @@
 /** Causes the caches to purge all entries and reload. */
 @RequiresAnyCapability({FLUSH_CACHES, MAINTAIN_SERVER})
 @CommandMetaData(
-  name = "flush-caches",
-  description = "Flush some/all server caches from memory",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "flush-caches",
+    description = "Flush some/all server caches from memory",
+    runsAt = MASTER_OR_SLAVE)
 final class FlushCaches extends SshCommand {
   @Option(name = "--cache", usage = "flush named cache", metaVar = "NAME")
   private List<String> caches = new ArrayList<>();
diff --git a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index 25f0e77..ecbb373 100644
--- a/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -49,12 +49,11 @@
   private boolean aggressive;
 
   @Argument(
-    index = 0,
-    required = false,
-    multiValued = true,
-    metaVar = "NAME",
-    usage = "projects for which the Git garbage collection should be run"
-  )
+      index = 0,
+      required = false,
+      multiValued = true,
+      metaVar = "NAME",
+      usage = "projects for which the Git garbage collection should be run")
   private List<ProjectState> projects = new ArrayList<>();
 
   @Inject private ProjectCache projectCache;
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 9a98257..fad74f5 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -36,12 +36,11 @@
   @Inject private ChangeArgumentParser changeArgumentParser;
 
   @Argument(
-    index = 0,
-    required = true,
-    multiValued = true,
-    metaVar = "CHANGE",
-    usage = "changes to index"
-  )
+      index = 0,
+      required = true,
+      multiValued = true,
+      metaVar = "CHANGE",
+      usage = "changes to index")
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, null, false);
diff --git a/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java b/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java
index e6abc17..407bbd0 100644
--- a/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java
@@ -34,12 +34,11 @@
   @Inject private Index index;
 
   @Argument(
-    index = 0,
-    required = true,
-    multiValued = true,
-    metaVar = "PROJECT",
-    usage = "projects for which the changes should be indexed"
-  )
+      index = 0,
+      required = true,
+      multiValued = true,
+      metaVar = "PROJECT",
+      usage = "projects for which the changes should be indexed")
   private List<ProjectState> projects = new ArrayList<>();
 
   @Override
diff --git a/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index 473cb0c..f3ba308 100644
--- a/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -33,23 +33,21 @@
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(
-  name = "ls-groups",
-  description = "List groups visible to the caller",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "ls-groups",
+    description = "List groups visible to the caller",
+    runsAt = MASTER_OR_SLAVE)
 public class ListGroupsCommand extends SshCommand {
   @Inject private GroupCache groupCache;
 
   @Inject @Options public ListGroups listGroups;
 
   @Option(
-    name = "--verbose",
-    aliases = {"-v"},
-    usage =
-        "verbose output format with tab-separated columns for the "
-            + "group name, UUID, description, owner group name, "
-            + "owner group UUID, and whether the group is visible to all"
-  )
+      name = "--verbose",
+      aliases = {"-v"},
+      usage =
+          "verbose output format with tab-separated columns for the "
+              + "group name, UUID, description, owner group name, "
+              + "owner group UUID, and whether the group is visible to all")
   private boolean verboseOutput;
 
   @Override
diff --git a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
index bb50fb1..c8b8fa1 100644
--- a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
@@ -29,10 +29,9 @@
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
-  name = "ls-level",
-  description = "list the level of loggers",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "ls-level",
+    description = "list the level of loggers",
+    runsAt = MASTER_OR_SLAVE)
 public class ListLoggingLevelCommand extends SshCommand {
 
   @Argument(index = 0, required = false, metaVar = "NAME", usage = "used to match loggers")
diff --git a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index bf3dd44..2d6b1b3 100644
--- a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -37,10 +37,9 @@
 
 /** Implements a command that allows the user to see the members of a account. */
 @CommandMetaData(
-  name = "ls-members",
-  description = "List the members of a given group",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "ls-members",
+    description = "List the members of a given group",
+    runsAt = MASTER_OR_SLAVE)
 public class ListMembersCommand extends SshCommand {
   @Inject ListMembersCommandImpl impl;
 
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 664f87b..d04e2d3 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -24,10 +24,9 @@
 import java.util.List;
 
 @CommandMetaData(
-  name = "ls-projects",
-  description = "List projects visible to the caller",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "ls-projects",
+    description = "List projects visible to the caller",
+    runsAt = MASTER_OR_SLAVE)
 public class ListProjectsCommand extends SshCommand {
   @Inject @Options public ListProjects impl;
 
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index b51e178..781679d 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -44,10 +44,9 @@
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
-  name = "ls-user-refs",
-  description = "List refs visible to a specific user",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "ls-user-refs",
+    description = "List refs visible to a specific user",
+    runsAt = MASTER_OR_SLAVE)
 public class LsUserRefs extends SshCommand {
   @Inject private AccountResolver accountResolver;
   @Inject private OneOffRequestContext requestContext;
@@ -55,21 +54,19 @@
   @Inject private GitRepositoryManager repoManager;
 
   @Option(
-    name = "--project",
-    aliases = {"-p"},
-    metaVar = "PROJECT",
-    required = true,
-    usage = "project for which the refs should be listed"
-  )
+      name = "--project",
+      aliases = {"-p"},
+      metaVar = "PROJECT",
+      required = true,
+      usage = "project for which the refs should be listed")
   private ProjectState projectState;
 
   @Option(
-    name = "--user",
-    aliases = {"-u"},
-    metaVar = "USER",
-    required = true,
-    usage = "user for which the groups should be listed"
-  )
+      name = "--user",
+      aliases = {"-u"},
+      metaVar = "USER",
+      required = true,
+      usage = "user for which the groups should be listed")
   private String userName;
 
   @Option(name = "--only-refs-heads", usage = "list only refs under refs/heads")
diff --git a/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 337eadb..8b045ec 100644
--- a/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -31,10 +31,9 @@
 @CommandMetaData(name = "install", description = "Install/Add a plugin", runsAt = MASTER_OR_SLAVE)
 final class PluginInstallCommand extends PluginAdminSshCommand {
   @Option(
-    name = "--name",
-    aliases = {"-n"},
-    usage = "install under name"
-  )
+      name = "--name",
+      aliases = {"-n"},
+      usage = "install under name")
   private String name;
 
   @Option(name = "-")
diff --git a/java/com/google/gerrit/sshd/commands/Query.java b/java/com/google/gerrit/sshd/commands/Query.java
index 2e5bf71..3fe0396 100644
--- a/java/com/google/gerrit/sshd/commands/Query.java
+++ b/java/com/google/gerrit/sshd/commands/Query.java
@@ -43,9 +43,8 @@
   }
 
   @Option(
-    name = "--all-approvals",
-    usage = "Include information about all patch sets and approvals"
-  )
+      name = "--all-approvals",
+      usage = "Include information about all patch sets and approvals")
   void setApprovals(boolean on) {
     if (on) {
       processor.setIncludePatchSets(on);
@@ -84,21 +83,19 @@
   }
 
   @Option(
-    name = "--start",
-    aliases = {"-S"},
-    usage = "Number of changes to skip"
-  )
+      name = "--start",
+      aliases = {"-S"},
+      usage = "Number of changes to skip")
   void setStart(int start) {
     processor.setStart(start);
   }
 
   @Argument(
-    index = 0,
-    required = true,
-    multiValued = true,
-    metaVar = "QUERY",
-    usage = "Query to execute"
-  )
+      index = 0,
+      required = true,
+      multiValued = true,
+      metaVar = "QUERY",
+      usage = "Query to execute")
   private List<String> query;
 
   @Override
diff --git a/java/com/google/gerrit/sshd/commands/Receive.java b/java/com/google/gerrit/sshd/commands/Receive.java
index a455c90..6c8dcd6 100644
--- a/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/java/com/google/gerrit/sshd/commands/Receive.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -41,16 +42,13 @@
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Receives change upload over SSH using the Git receive-pack protocol. */
 @CommandMetaData(
-  name = "receive-pack",
-  description = "Standard Git server side command for client side git push"
-)
+    name = "receive-pack",
+    description = "Standard Git server side command for client side git push")
 final class Receive extends AbstractGitCommand {
-  private static final Logger log = LoggerFactory.getLogger(Receive.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Inject private AsyncReceiveCommits.Factory factory;
   @Inject private IdentifiedUser currentUser;
@@ -61,21 +59,19 @@
       MultimapBuilder.hashKeys(2).hashSetValues().build();
 
   @Option(
-    name = "--reviewer",
-    aliases = {"--re"},
-    metaVar = "EMAIL",
-    usage = "request reviewer for change(s)"
-  )
+      name = "--reviewer",
+      aliases = {"--re"},
+      metaVar = "EMAIL",
+      usage = "request reviewer for change(s)")
   void addReviewer(Account.Id id) {
     reviewers.put(ReviewerStateInternal.REVIEWER, id);
   }
 
   @Option(
-    name = "--cc",
-    aliases = {},
-    metaVar = "EMAIL",
-    usage = "CC user on change(s)"
-  )
+      name = "--cc",
+      aliases = {},
+      metaVar = "EMAIL",
+      usage = "CC user on change(s)")
   void addCC(Account.Id id) {
     reviewers.put(ReviewerStateInternal.CC, id);
   }
@@ -121,7 +117,7 @@
         msg.append(currentUser.getAccountId());
         msg.append("): ");
         msg.append(badStream.getCause().getMessage());
-        log.info(msg.toString());
+        logger.atInfo().log(msg.toString());
         throw new UnloggedFailure(128, "error: " + badStream.getCause().getMessage());
       }
 
diff --git a/java/com/google/gerrit/sshd/commands/ReloadConfig.java b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
index 20145d2..1b21230 100644
--- a/java/com/google/gerrit/sshd/commands/ReloadConfig.java
+++ b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
@@ -30,10 +30,9 @@
 /** Issues a reload of gerrit.config. */
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
-  name = "reload-config",
-  description = "Reloads the Gerrit configuration",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "reload-config",
+    description = "Reloads the Gerrit configuration",
+    runsAt = MASTER_OR_SLAVE)
 public class ReloadConfig extends SshCommand {
 
   @Inject private GerritServerConfigReloader gerritServerConfigReloader;
diff --git a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index cd9fbda..166ad68 100644
--- a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -31,11 +31,10 @@
 @CommandMetaData(name = "rename-group", description = "Rename an account group")
 public class RenameGroupCommand extends SshCommand {
   @Argument(
-    index = 0,
-    required = true,
-    metaVar = "GROUP",
-    usage = "name of the group to be renamed"
-  )
+      index = 0,
+      required = true,
+      metaVar = "GROUP",
+      usage = "name of the group to be renamed")
   private String groupName;
 
   @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "new name of the group")
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 1d764b9..d5b44b5 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
@@ -53,12 +54,10 @@
 import java.util.TreeMap;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @CommandMetaData(name = "review", description = "Apply reviews to one or more patch sets")
 public class ReviewCommand extends SshCommand {
-  private static final Logger log = LoggerFactory.getLogger(ReviewCommand.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Override
   protected final CmdLineParser newCmdLineParser(Object options) {
@@ -72,12 +71,11 @@
   private final Set<PatchSet> patchSets = new HashSet<>();
 
   @Argument(
-    index = 0,
-    required = true,
-    multiValued = true,
-    metaVar = "{COMMIT | CHANGE,PATCHSET}",
-    usage = "list of commits or patch sets to review"
-  )
+      index = 0,
+      required = true,
+      multiValued = true,
+      metaVar = "{COMMIT | CHANGE,PATCHSET}",
+      usage = "list of commits or patch sets to review")
   void addPatchSetId(String token) {
     try {
       PatchSet ps = psParser.parsePatchSet(token, projectState, branch);
@@ -90,29 +88,26 @@
   }
 
   @Option(
-    name = "--project",
-    aliases = "-p",
-    usage = "project containing the specified patch set(s)"
-  )
+      name = "--project",
+      aliases = "-p",
+      usage = "project containing the specified patch set(s)")
   private ProjectState projectState;
 
   @Option(name = "--branch", aliases = "-b", usage = "branch containing the specified patch set(s)")
   private String branch;
 
   @Option(
-    name = "--message",
-    aliases = "-m",
-    usage = "cover message to publish on change(s)",
-    metaVar = "MESSAGE"
-  )
+      name = "--message",
+      aliases = "-m",
+      usage = "cover message to publish on change(s)",
+      metaVar = "MESSAGE")
   private String changeComment;
 
   @Option(
-    name = "--notify",
-    aliases = "-n",
-    usage = "Who to send email notifications to after the review is stored.",
-    metaVar = "NOTIFYHANDLING"
-  )
+      name = "--notify",
+      aliases = "-n",
+      usage = "Who to send email notifications to after the review is stored.",
+      metaVar = "NOTIFYHANDLING")
   private NotifyHandling notify;
 
   @Option(name = "--abandon", usage = "abandon the specified change(s)")
@@ -134,19 +129,17 @@
   private boolean json;
 
   @Option(
-    name = "--tag",
-    aliases = "-t",
-    usage = "applies a tag to the given review",
-    metaVar = "TAG"
-  )
+      name = "--tag",
+      aliases = "-t",
+      usage = "applies a tag to the given review",
+      metaVar = "TAG")
   private String changeTag;
 
   @Option(
-    name = "--label",
-    aliases = "-l",
-    usage = "custom label(s) to assign",
-    metaVar = "LABEL=VALUE"
-  )
+      name = "--label",
+      aliases = "-l",
+      usage = "custom label(s) to assign",
+      metaVar = "LABEL=VALUE")
   void addLabel(String token) {
     LabelVote v = LabelVote.parseWithEquals(token);
     LabelType.checkName(v.label()); // Disallow SUBM.
@@ -231,7 +224,7 @@
       } catch (Exception e) {
         ok = false;
         writeError("fatal", "internal server error while reviewing " + patchSet.getId() + "\n");
-        log.error("internal error while reviewing " + patchSet.getId(), e);
+        logger.atSevere().withCause(e).log("internal error while reviewing %s", patchSet.getId());
       }
     }
 
diff --git a/java/com/google/gerrit/sshd/commands/ScpCommand.java b/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 1306c52..89a09ef 100644
--- a/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -24,6 +24,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.tools.ToolsCatalog.Entry;
 import com.google.gerrit.sshd.BaseCommand;
@@ -33,13 +34,12 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import org.apache.sshd.server.Environment;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 final class ScpCommand extends BaseCommand {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String TYPE_DIR = "D";
   private static final String TYPE_FILE = "C";
-  private static final Logger log = LoggerFactory.getLogger(ScpCommand.class);
 
   private boolean opt_r;
   private boolean opt_t;
@@ -137,7 +137,7 @@
       } catch (IOException e2) {
         // Ignore
       }
-      log.debug("Error in scp command", e);
+      logger.atFine().withCause(e).log("Error in scp command");
     }
   }
 
@@ -216,7 +216,7 @@
       case 0:
         break;
       case 1:
-        log.debug("Received warning: " + readLine());
+        logger.atFine().log("Received warning: %s", readLine());
         break;
       case 2:
         throw new IOException("Received nack: " + readLine());
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index bc1e084..379fc68 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -69,11 +69,10 @@
 final class SetAccountCommand extends SshCommand {
 
   @Argument(
-    index = 0,
-    required = true,
-    metaVar = "USER",
-    usage = "full name, email-address, ssh username or account id"
-  )
+      index = 0,
+      required = true,
+      metaVar = "USER",
+      usage = "full name, email-address, ssh username or account id")
   private Account.Id id;
 
   @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
@@ -89,34 +88,30 @@
   private List<String> addEmails = new ArrayList<>();
 
   @Option(
-    name = "--delete-email",
-    metaVar = "EMAIL",
-    usage = "email addresses to delete from the account"
-  )
+      name = "--delete-email",
+      metaVar = "EMAIL",
+      usage = "email addresses to delete from the account")
   private List<String> deleteEmails = new ArrayList<>();
 
   @Option(
-    name = "--preferred-email",
-    metaVar = "EMAIL",
-    usage = "a registered email address from the account"
-  )
+      name = "--preferred-email",
+      metaVar = "EMAIL",
+      usage = "a registered email address from the account")
   private String preferredEmail;
 
   @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account")
   private List<String> addSshKeys = new ArrayList<>();
 
   @Option(
-    name = "--delete-ssh-key",
-    metaVar = "-|KEY",
-    usage = "public keys to delete from the account"
-  )
+      name = "--delete-ssh-key",
+      metaVar = "-|KEY",
+      usage = "public keys to delete from the account")
   private List<String> deleteSshKeys = new ArrayList<>();
 
   @Option(
-    name = "--http-password",
-    metaVar = "PASSWORD",
-    usage = "password for HTTP authentication for the account"
-  )
+      name = "--http-password",
+      metaVar = "PASSWORD",
+      usage = "password for HTTP authentication for the account")
   private String httpPassword;
 
   @Option(name = "--clear-http-password", usage = "clear HTTP password for the account")
diff --git a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index bea4da13..cfdd735 100644
--- a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -33,10 +33,9 @@
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
-  name = "set-level",
-  description = "Change the level of loggers",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "set-level",
+    description = "Change the level of loggers",
+    runsAt = MASTER_OR_SLAVE)
 public class SetLoggingLevelCommand extends SshCommand {
   private static final String LOG_CONFIGURATION = "log4j.properties";
   private static final String JAVA_OPTIONS_LOG_CONFIG = "log4j.configuration";
diff --git a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index 2cb4114..9d7f2d9 100644
--- a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -46,50 +46,44 @@
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(
-  name = "set-members",
-  description = "Modify members of specific group or number of groups"
-)
+    name = "set-members",
+    description = "Modify members of specific group or number of groups")
 public class SetMembersCommand extends SshCommand {
 
   @Option(
-    name = "--add",
-    aliases = {"-a"},
-    metaVar = "USER",
-    usage = "users that should be added as group member"
-  )
+      name = "--add",
+      aliases = {"-a"},
+      metaVar = "USER",
+      usage = "users that should be added as group member")
   private List<Account.Id> accountsToAdd = new ArrayList<>();
 
   @Option(
-    name = "--remove",
-    aliases = {"-r"},
-    metaVar = "USER",
-    usage = "users that should be removed from the group"
-  )
+      name = "--remove",
+      aliases = {"-r"},
+      metaVar = "USER",
+      usage = "users that should be removed from the group")
   private List<Account.Id> accountsToRemove = new ArrayList<>();
 
   @Option(
-    name = "--include",
-    aliases = {"-i"},
-    metaVar = "GROUP",
-    usage = "group that should be included as group member"
-  )
+      name = "--include",
+      aliases = {"-i"},
+      metaVar = "GROUP",
+      usage = "group that should be included as group member")
   private List<AccountGroup.UUID> groupsToInclude = new ArrayList<>();
 
   @Option(
-    name = "--exclude",
-    aliases = {"-e"},
-    metaVar = "GROUP",
-    usage = "group that should be excluded from the group"
-  )
+      name = "--exclude",
+      aliases = {"-e"},
+      metaVar = "GROUP",
+      usage = "group that should be excluded from the group")
   private List<AccountGroup.UUID> groupsToRemove = new ArrayList<>();
 
   @Argument(
-    index = 0,
-    required = true,
-    multiValued = true,
-    metaVar = "GROUP",
-    usage = "groups to modify"
-  )
+      index = 0,
+      required = true,
+      multiValued = true,
+      metaVar = "GROUP",
+      usage = "groups to modify")
   private List<AccountGroup.UUID> groups = new ArrayList<>();
 
   @Inject private AddMembers addMembers;
diff --git a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index a5759f0..1e177a1 100644
--- a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -35,18 +35,16 @@
   private ProjectState projectState;
 
   @Option(
-    name = "--description",
-    aliases = {"-d"},
-    metaVar = "DESCRIPTION",
-    usage = "description of project"
-  )
+      name = "--description",
+      aliases = {"-d"},
+      metaVar = "DESCRIPTION",
+      usage = "description of project")
   private String projectDescription;
 
   @Option(
-    name = "--submit-type",
-    aliases = {"-t"},
-    usage = "project submit type\n(default: MERGE_IF_NECESSARY)"
-  )
+      name = "--submit-type",
+      aliases = {"-t"},
+      usage = "project submit type\n(default: MERGE_IF_NECESSARY)")
   private SubmitType submitType;
 
   @Option(name = "--contributor-agreements", usage = "if contributor agreement is required")
@@ -62,37 +60,33 @@
   private InheritableBoolean requireChangeID;
 
   @Option(
-    name = "--use-contributor-agreements",
-    aliases = {"--ca"},
-    usage = "if contributor agreement is required"
-  )
+      name = "--use-contributor-agreements",
+      aliases = {"--ca"},
+      usage = "if contributor agreement is required")
   void setUseContributorArgreements(@SuppressWarnings("unused") boolean on) {
     contributorAgreements = InheritableBoolean.TRUE;
   }
 
   @Option(
-    name = "--no-contributor-agreements",
-    aliases = {"--nca"},
-    usage = "if contributor agreement is not required"
-  )
+      name = "--no-contributor-agreements",
+      aliases = {"--nca"},
+      usage = "if contributor agreement is not required")
   void setNoContributorArgreements(@SuppressWarnings("unused") boolean on) {
     contributorAgreements = InheritableBoolean.FALSE;
   }
 
   @Option(
-    name = "--use-signed-off-by",
-    aliases = {"--so"},
-    usage = "if signed-off-by is required"
-  )
+      name = "--use-signed-off-by",
+      aliases = {"--so"},
+      usage = "if signed-off-by is required")
   void setUseSignedOffBy(@SuppressWarnings("unused") boolean on) {
     signedOffBy = InheritableBoolean.TRUE;
   }
 
   @Option(
-    name = "--no-signed-off-by",
-    aliases = {"--nso"},
-    usage = "if signed-off-by is not required"
-  )
+      name = "--no-signed-off-by",
+      aliases = {"--nso"},
+      usage = "if signed-off-by is not required")
   void setNoSignedOffBy(@SuppressWarnings("unused") boolean on) {
     signedOffBy = InheritableBoolean.FALSE;
   }
@@ -103,36 +97,32 @@
   }
 
   @Option(
-    name = "--no-content-merge",
-    usage = "don't allow automatic conflict resolving within files"
-  )
+      name = "--no-content-merge",
+      usage = "don't allow automatic conflict resolving within files")
   void setNoContentMerge(@SuppressWarnings("unused") boolean on) {
     contentMerge = InheritableBoolean.FALSE;
   }
 
   @Option(
-    name = "--require-change-id",
-    aliases = {"--id"},
-    usage = "if change-id is required"
-  )
+      name = "--require-change-id",
+      aliases = {"--id"},
+      usage = "if change-id is required")
   void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
     requireChangeID = InheritableBoolean.TRUE;
   }
 
   @Option(
-    name = "--no-change-id",
-    aliases = {"--nid"},
-    usage = "if change-id is not required"
-  )
+      name = "--no-change-id",
+      aliases = {"--nid"},
+      usage = "if change-id is not required")
   void setNoChangeId(@SuppressWarnings("unused") boolean on) {
     requireChangeID = InheritableBoolean.FALSE;
   }
 
   @Option(
-    name = "--project-state",
-    aliases = {"--ps"},
-    usage = "project's visibility state"
-  )
+      name = "--project-state",
+      aliases = {"--ps"},
+      usage = "project's visibility state")
   private ProjectState state;
 
   @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index c777afa..a4a8ea8 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -39,41 +40,36 @@
 import java.util.Set;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @CommandMetaData(name = "set-reviewers", description = "Add or remove reviewers on a change")
 public class SetReviewersCommand extends SshCommand {
-  private static final Logger log = LoggerFactory.getLogger(SetReviewersCommand.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Option(name = "--project", aliases = "-p", usage = "project containing the change")
   private ProjectState projectState;
 
   @Option(
-    name = "--add",
-    aliases = {"-a"},
-    metaVar = "REVIEWER",
-    usage = "user or group that should be added as reviewer"
-  )
+      name = "--add",
+      aliases = {"-a"},
+      metaVar = "REVIEWER",
+      usage = "user or group that should be added as reviewer")
   private List<String> toAdd = new ArrayList<>();
 
   @Option(
-    name = "--remove",
-    aliases = {"-r"},
-    metaVar = "REVIEWER",
-    usage = "user that should be removed from the reviewer list"
-  )
+      name = "--remove",
+      aliases = {"-r"},
+      metaVar = "REVIEWER",
+      usage = "user that should be removed from the reviewer list")
   void optionRemove(Account.Id who) {
     toRemove.add(who);
   }
 
   @Argument(
-    index = 0,
-    required = true,
-    multiValued = true,
-    metaVar = "CHANGE",
-    usage = "changes to modify"
-  )
+      index = 0,
+      required = true,
+      multiValued = true,
+      metaVar = "CHANGE",
+      usage = "changes to modify")
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, projectState);
@@ -106,7 +102,7 @@
         ok &= modifyOne(rsrc);
       } catch (Exception err) {
         ok = false;
-        log.error("Error updating reviewers on change " + rsrc.getId(), err);
+        logger.atSevere().withCause(err).log("Error updating reviewers on change %s", rsrc.getId());
         writeError("fatal", "internal error while updating " + rsrc.getId());
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index a356f7f..3c95884 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -57,10 +57,9 @@
 /** Show the current cache states. */
 @RequiresAnyCapability({VIEW_CACHES, MAINTAIN_SERVER})
 @CommandMetaData(
-  name = "show-caches",
-  description = "Display current cache statistics",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "show-caches",
+    description = "Display current cache statistics",
+    runsAt = MASTER_OR_SLAVE)
 final class ShowCaches extends SshCommand {
   private static volatile long serverStarted;
 
@@ -90,11 +89,10 @@
   @Inject private PermissionBackend permissionBackend;
 
   @Option(
-    name = "--width",
-    aliases = {"-w"},
-    metaVar = "COLS",
-    usage = "width of output table"
-  )
+      name = "--width",
+      aliases = {"-w"},
+      metaVar = "COLS",
+      usage = "width of output table")
   private int columns = 80;
 
   private int nw;
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index c5485ef..b30799b 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -51,23 +51,20 @@
 /** Show the current SSH connections. */
 @RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
 @CommandMetaData(
-  name = "show-connections",
-  description = "Display active client SSH connections",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "show-connections",
+    description = "Display active client SSH connections",
+    runsAt = MASTER_OR_SLAVE)
 final class ShowConnections extends SshCommand {
   @Option(
-    name = "--numeric",
-    aliases = {"-n"},
-    usage = "don't resolve names"
-  )
+      name = "--numeric",
+      aliases = {"-n"},
+      usage = "don't resolve names")
   private boolean numeric;
 
   @Option(
-    name = "--wide",
-    aliases = {"-w"},
-    usage = "display without line width truncation"
-  )
+      name = "--wide",
+      aliases = {"-w"},
+      usage = "display without line width truncation")
   private boolean wide;
 
   @Inject private SshDaemon daemon;
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 6d2fbb4..5d7fdbf 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -45,23 +45,20 @@
 /** Display the current work queue. */
 @AdminHighPriorityCommand
 @CommandMetaData(
-  name = "show-queue",
-  description = "Display the background work queues",
-  runsAt = MASTER_OR_SLAVE
-)
+    name = "show-queue",
+    description = "Display the background work queues",
+    runsAt = MASTER_OR_SLAVE)
 final class ShowQueue extends SshCommand {
   @Option(
-    name = "--wide",
-    aliases = {"-w"},
-    usage = "display without line width truncation"
-  )
+      name = "--wide",
+      aliases = {"-w"},
+      usage = "display without line width truncation")
   private boolean wide;
 
   @Option(
-    name = "--by-queue",
-    aliases = {"-q"},
-    usage = "group tasks by queue and print queue info"
-  )
+      name = "--by-queue",
+      aliases = {"-q"},
+      usage = "group tasks by queue and print queue info")
   private boolean groupByQueue;
 
   @Inject private PermissionBackend permissionBackend;
diff --git a/java/com/google/gerrit/sshd/commands/StreamEvents.java b/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 9e8e85e..c97372c 100644
--- a/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Supplier;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -45,13 +46,11 @@
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RequiresCapability(GlobalCapability.STREAM_EVENTS)
 @CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
-  private static final Logger log = LoggerFactory.getLogger(StreamEvents.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
@@ -60,11 +59,10 @@
   private static final int BATCH_SIZE = 32;
 
   @Option(
-    name = "--subscribe",
-    aliases = {"-s"},
-    metaVar = "SUBSCRIBE",
-    usage = "subscribe to specific stream-events"
-  )
+      name = "--subscribe",
+      aliases = {"-s"},
+      metaVar = "SUBSCRIBE",
+      usage = "subscribe to specific stream-events")
   private List<String> subscribedToEvents = new ArrayList<>();
 
   @Inject private IdentifiedUser currentUser;
@@ -279,7 +277,7 @@
     try {
       msg = gson.toJson(message) + "\n";
     } catch (Exception e) {
-      log.warn("Could not deserialize the msg: ", e);
+      logger.atWarning().withCause(e).log("Could not deserialize the msg");
     }
     if (msg != null) {
       synchronized (stdout) {
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index c838c16..91b190f 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -59,15 +59,14 @@
    */
   static class Options {
     @Option(
-      name = "-f",
-      aliases = {"--format"},
-      usage =
-          "Format of the"
-              + " resulting archive: tar or zip... If this option is not given, and"
-              + " the output file is specified, the format is inferred from the"
-              + " filename if possible (e.g. writing to \"foo.zip\" makes the output"
-              + " to be in the zip format). Otherwise the output format is tar."
-    )
+        name = "-f",
+        aliases = {"--format"},
+        usage =
+            "Format of the"
+                + " resulting archive: tar or zip... If this option is not given, and"
+                + " the output file is specified, the format is inferred from the"
+                + " filename if possible (e.g. writing to \"foo.zip\" makes the output"
+                + " to be in the zip format). Otherwise the output format is tar.")
     private String format = "tar";
 
     @Option(name = "--prefix", usage = "Prepend <prefix>/ to each filename in the archive.")
@@ -101,25 +100,23 @@
     private boolean level8;
 
     @Option(
-      name = "-9",
-      usage =
-          "Highest and slowest compression level. You "
-              + "can specify any number from 1 to 9 to adjust compression speed and "
-              + "ratio."
-    )
+        name = "-9",
+        usage =
+            "Highest and slowest compression level. You "
+                + "can specify any number from 1 to 9 to adjust compression speed and "
+                + "ratio.")
     private boolean level9;
 
     @Argument(index = 0, required = true, usage = "The tree or commit to produce an archive for.")
     private String treeIsh = "master";
 
     @Argument(
-      index = 1,
-      multiValued = true,
-      usage =
-          "Without an optional path parameter, all files and subdirectories of "
-              + "the current working directory are included in the archive. If one "
-              + "or more paths are specified, only these are included."
-    )
+        index = 1,
+        multiValued = true,
+        usage =
+            "Without an optional path parameter, all files and subdirectories of "
+                + "the current working directory are included in the archive. If one "
+                + "or more paths are specified, only these are included.")
     private List<String> path;
   }
 
diff --git a/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java b/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
index 1858f40..8fb2461 100644
--- a/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
+++ b/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.plugin;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -24,11 +25,10 @@
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Argument;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class LfsPluginAuthCommand extends SshCommand {
-  private static final Logger log = LoggerFactory.getLogger(LfsPluginAuthCommand.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final String CONFIGURATION_ERROR =
       "Server configuration error: LFS auth over SSH is not properly configured.";
 
@@ -67,7 +67,7 @@
   protected void run() throws UnloggedFailure, Exception {
     LfsSshPluginAuth pluginAuth = auth.get();
     if (pluginAuth == null) {
-      log.warn(CONFIGURATION_ERROR);
+      logger.atWarning().log(CONFIGURATION_ERROR);
       throw new UnloggedFailure(1, CONFIGURATION_ERROR);
     }
 
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 875d636..43aa978 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -35,11 +35,11 @@
         "//lib:junit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
-        "//lib/log:api",
         "//lib/truth",
     ],
 )
diff --git a/java/com/google/gerrit/testing/ConfigSuite.java b/java/com/google/gerrit/testing/ConfigSuite.java
index a816c80..b0229c3 100644
--- a/java/com/google/gerrit/testing/ConfigSuite.java
+++ b/java/com/google/gerrit/testing/ConfigSuite.java
@@ -104,6 +104,14 @@
  * field annotated with {@code @ConfigSuite.Name}.
  */
 public class ConfigSuite extends Suite {
+  private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
+
+  static {
+    System.setProperty(
+        FLOGGER_BACKEND_PROPERTY,
+        "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");
+  }
+
   public static final String DEFAULT = "default";
 
   @Target({METHOD})
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index 19e85db..28946dc 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -19,6 +19,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.git.WorkQueue;
@@ -35,8 +36,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Email sender implementation that records messages in memory.
@@ -48,7 +47,7 @@
  */
 @Singleton
 public class FakeEmailSender implements EmailSender {
-  private static final Logger log = LoggerFactory.getLogger(FakeEmailSender.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static class Module extends AbstractModule {
     @Override
@@ -166,7 +165,7 @@
         try {
           task.get();
         } catch (ExecutionException | InterruptedException e) {
-          log.warn("error finishing email task", e);
+          logger.atWarning().withCause(e).log("error finishing email task");
         }
       }
     }
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b472857..b3f6222 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.api.GerritApiModule;
 import com.google.gerrit.server.api.PluginApiModule;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
@@ -48,6 +49,7 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritInstanceNameModule;
 import com.google.gerrit.server.config.GerritOptions;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.config.GerritServerIdProvider;
@@ -74,7 +76,6 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffExecutor;
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
-import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.plugins.ServerInformationImpl;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
 import com.google.gerrit.server.restapi.RestApiModule;
@@ -168,9 +169,11 @@
                 bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
               }
             });
+    bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
     bind(MetricMaker.class).to(DisabledMetricMaker.class);
     install(cfgInjector.getInstance(GerritGlobalModule.class));
     install(new GerritApiModule());
+    factory(PluginUser.Factory.class);
     install(new PluginApiModule());
     install(new DefaultPermissionBackendModule());
     install(new SearchingChangeCacheImpl.Module());
@@ -264,7 +267,6 @@
     bind(ServerInformationImpl.class);
     bind(ServerInformation.class).to(ServerInformationImpl.class);
     install(new RestApiModule());
-    install(new PluginRestApiModule());
     install(new DefaultProjectNameLockManager.Module());
   }
 
diff --git a/java/com/google/gerrit/testing/NoteDbChecker.java b/java/com/google/gerrit/testing/NoteDbChecker.java
index b5dd9e9..1dc8ee2 100644
--- a/java/com/google/gerrit/testing/NoteDbChecker.java
+++ b/java/com/google/gerrit/testing/NoteDbChecker.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -46,12 +47,10 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.runner.Description;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class NoteDbChecker {
-  static final Logger log = LoggerFactory.getLogger(NoteDbChecker.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
@@ -193,7 +192,7 @@
         } catch (Throwable t) {
           String msg = "Error converting change: " + c;
           msgs.add(msg);
-          log.error(msg, t);
+          logger.atSevere().withCause(t).log(msg);
           continue;
         }
         List<String> diff = expected.differencesFrom(actual);
diff --git a/java/com/google/gwtexpui/server/CacheHeaders.java b/java/com/google/gerrit/util/http/CacheHeaders.java
similarity index 98%
rename from java/com/google/gwtexpui/server/CacheHeaders.java
rename to java/com/google/gerrit/util/http/CacheHeaders.java
index 0e5e425..454587c 100644
--- a/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/java/com/google/gerrit/util/http/CacheHeaders.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gwtexpui.server;
+package com.google.gerrit.util.http;
 
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.SECONDS;
diff --git a/java/com/google/gerrit/util/ssl/BlindHostnameVerifier.java b/java/com/google/gerrit/util/ssl/BlindHostnameVerifier.java
new file mode 100644
index 0000000..ac758690
--- /dev/null
+++ b/java/com/google/gerrit/util/ssl/BlindHostnameVerifier.java
@@ -0,0 +1,33 @@
+// 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.util.ssl;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+/** HostnameVerifier that ignores host name. */
+public class BlindHostnameVerifier implements HostnameVerifier {
+
+  private static final HostnameVerifier INSTANCE = new BlindHostnameVerifier();
+
+  public static HostnameVerifier getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public boolean verify(String hostname, SSLSession session) {
+    return true;
+  }
+}
diff --git a/java/com/google/gwtexpui/linker/BUILD b/java/com/google/gwtexpui/linker/BUILD
deleted file mode 100644
index 5c5c600..0000000
--- a/java/com/google/gwtexpui/linker/BUILD
+++ /dev/null
@@ -1,6 +0,0 @@
-java_library(
-    name = "server",
-    srcs = glob(["server/*.java"]),
-    visibility = ["//visibility:public"],
-    deps = ["//lib:servlet-api-3_1"],
-)
diff --git a/java/com/google/gwtexpui/server/BUILD b/java/com/google/gwtexpui/server/BUILD
deleted file mode 100644
index 9b81564..0000000
--- a/java/com/google/gwtexpui/server/BUILD
+++ /dev/null
@@ -1,6 +0,0 @@
-java_library(
-    name = "server",
-    srcs = glob(["**/*.java"]),
-    visibility = ["//visibility:public"],
-    deps = ["//lib:servlet-api-3_1"],
-)
diff --git a/java/gerrit/BUILD b/java/gerrit/BUILD
index 980ad23..4644af87 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/BUILD
@@ -8,8 +8,8 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:gwtorm",
+        "//lib/flogger:api",
         "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/log:api",
         "//lib/prolog:runtime",
     ],
 )
diff --git a/java/gerrit/PRED_uploader_1.java b/java/gerrit/PRED_uploader_1.java
index bf1bf27..029b84a 100644
--- a/java/gerrit/PRED_uploader_1.java
+++ b/java/gerrit/PRED_uploader_1.java
@@ -14,6 +14,7 @@
 
 package gerrit;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.rules.StoredValues;
@@ -25,11 +26,9 @@
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class PRED_uploader_1 extends Predicate.P1 {
-  private static final Logger log = LoggerFactory.getLogger(PRED_uploader_1.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final SymbolTerm user = SymbolTerm.intern("user", 1);
 
@@ -45,9 +44,9 @@
 
     PatchSet patchSet = StoredValues.getPatchSet(engine);
     if (patchSet == null) {
-      log.error(
-          "Failed to load current patch set of change "
-              + StoredValues.getChange(engine).getChangeId());
+      logger.atSevere().log(
+          "Failed to load current patch set of change %s",
+          StoredValues.getChange(engine).getChangeId());
       return engine.fail();
     }
 
diff --git a/java/org/apache/commons/net/BUILD b/java/org/apache/commons/net/BUILD
index 0074a03..4951933 100644
--- a/java/org/apache/commons/net/BUILD
+++ b/java/org/apache/commons/net/BUILD
@@ -6,6 +6,5 @@
         "//java/com/google/gerrit/util/ssl",
         "//lib/commons:codec",
         "//lib/commons:net",
-        "//lib/log:api",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
index 53f1839..d5ac2f7 100644
--- a/javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
@@ -47,9 +47,8 @@
 
   @Test
   @GerritConfig(
-    name = "section.name",
-    values = {"value-1", "value-2"}
-  )
+      name = "section.name",
+      values = {"value-1", "value-2"})
   public void testList() {
     assertThat(cfg.getStringList("section", null, "name"))
         .asList()
@@ -58,9 +57,8 @@
 
   @Test
   @GerritConfig(
-    name = "section.subsection.name",
-    values = {"value-1", "value-2"}
-  )
+      name = "section.subsection.name",
+      values = {"value-1", "value-2"})
   public void testListWithSubsection() {
     assertThat(cfg.getStringList("section", "subsection", "name"))
         .asList()
@@ -69,10 +67,9 @@
 
   @Test
   @GerritConfig(
-    name = "section.name",
-    value = "value-1",
-    values = {"value-2", "value-3"}
-  )
+      name = "section.name",
+      value = "value-1",
+      values = {"value-2", "value-3"})
   public void valueHasPrecedenceOverValues() {
     assertThat(cfg.getStringList("section", null, "name")).asList().containsExactly("value-1");
   }
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java
index eaa0a95d..44d9e46 100644
--- a/javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseGlobalPluginConfigAnnotationTest.java
@@ -57,10 +57,9 @@
   @Test
   @UseLocalDisk
   @GlobalPluginConfig(
-    pluginName = "test",
-    name = "section.name",
-    values = {"value-1", "value-2"}
-  )
+      pluginName = "test",
+      name = "section.name",
+      values = {"value-1", "value-2"})
   public void testList() {
     assertThat(cfg().getStringList("section", null, "name"))
         .asList()
@@ -70,10 +69,9 @@
   @Test
   @UseLocalDisk
   @GlobalPluginConfig(
-    pluginName = "test",
-    name = "section.subsection.name",
-    values = {"value-1", "value-2"}
-  )
+      pluginName = "test",
+      name = "section.subsection.name",
+      values = {"value-1", "value-2"})
   public void testListWithSubsection() {
     assertThat(cfg().getStringList("section", "subsection", "name"))
         .asList()
@@ -83,11 +81,10 @@
   @Test
   @UseLocalDisk
   @GlobalPluginConfig(
-    pluginName = "test",
-    name = "section.name",
-    value = "value-1",
-    values = {"value-2", "value-3"}
-  )
+      pluginName = "test",
+      name = "section.name",
+      value = "value-1",
+      values = {"value-2", "value-3"})
   public void valueHasPrecedenceOverValues() {
     assertThat(cfg().getStringList("section", null, "name")).asList().containsExactly("value-1");
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 3c8dba2..1d0f3ac 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -728,6 +728,17 @@
   }
 
   @Test
+  public void deleteStarLabelsFromChangeWithoutStarLabels() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
+
+    gApi.accounts().self().setStars(triplet, new StarsInput());
+
+    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
+  }
+
+  @Test
   public void starWithDefaultAndIgnoreLabel() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
@@ -916,9 +927,8 @@
 
   @Test
   @GerritConfig(
-    name = "auth.registerEmailPrivateKey",
-    value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co="
-  )
+      name = "auth.registerEmailPrivateKey",
+      value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailSendsConfirmationEmail() throws Exception {
     String email = "new.email@example.com";
     EmailInput input = newEmailInput(email, false);
@@ -931,9 +941,8 @@
 
   @Test
   @GerritConfig(
-    name = "auth.registerEmailPrivateKey",
-    value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co="
-  )
+      name = "auth.registerEmailPrivateKey",
+      value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOwnAccount() throws Exception {
     TestAccount user = accountCreator.create();
     setApiUser(user);
@@ -956,9 +965,8 @@
 
   @Test
   @GerritConfig(
-    name = "auth.registerEmailPrivateKey",
-    value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co="
-  )
+      name = "auth.registerEmailPrivateKey",
+      value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOtherAccount() throws Exception {
     TestAccount user = accountCreator.create();
     String email = "me@example.com";
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 882996e..b85e2f2 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -2676,7 +2676,9 @@
 
       assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id), c.updated, serverIdent.get());
+          changeNoteUtil
+              .getLegacyChangeNoteWrite()
+              .newIdent(getAccount(admin.id), c.updated, serverIdent.get());
       assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitPatchSetCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -2684,7 +2686,10 @@
 
       RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0));
       assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
-      expectedAuthor = changeNoteUtil.newIdent(getAccount(admin.id), c.created, serverIdent.get());
+      expectedAuthor =
+          changeNoteUtil
+              .getLegacyChangeNoteWrite()
+              .newIdent(getAccount(admin.id), c.created, serverIdent.get());
       assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitChangeCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
index 0b7f340..fe7da66 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -124,9 +124,8 @@
 
   @Test
   @GerritConfig(
-    name = "change.api.allowedIdentifier",
-    values = {"PROJECT_NUMERIC_ID", "NUMERIC_ID"}
-  )
+      name = "change.api.allowedIdentifier",
+      values = {"PROJECT_NUMERIC_ID", "NUMERIC_ID"})
   public void deprecatedChangeIdReturnsBadRequest() throws Exception {
     // project~changeNumber still works
     ChangeApi cApi1 = gApi.changes().id(project.get(), changeInfo._number);
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 1d7f3d9..8cc5c00 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -276,7 +276,7 @@
 
   @Test
   public void output() throws Exception {
-    String url = canonicalWebUrl.get() + "#/c/" + project.get() + "/+/";
+    String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/";
     ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
     PushOneCommit.Result r1 = pushTo("refs/for/master");
     Change.Id id1 = r1.getChange().getId();
@@ -328,7 +328,7 @@
     assertPushOk(pushHead(testRepo, master, false), master);
 
     // Attempt to push amended commit to same change
-    String url = canonicalWebUrl.get() + "#/c/" + project.get() + "/+/" + r.getChange().getId();
+    String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/" + r.getChange().getId();
     r = amendChange(r.getChangeId(), "refs/for/master");
     r.assertErrorStatus("change " + url + " closed");
 
@@ -354,7 +354,7 @@
     assertPushOk(pushHead(testRepo, master, false), master);
 
     // Attempt to push amended commit to same change
-    String url = canonicalWebUrl.get() + "#/c/" + project.get() + "/+/" + r.getChange().getId();
+    String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/" + r.getChange().getId();
     r = amendChange(r.getChangeId(), "refs/for/master");
     r.assertErrorStatus("change " + url + " closed");
 
@@ -700,7 +700,7 @@
     r.assertMessage(
         "Updated Changes:\n  "
             + canonicalWebUrl.get()
-            + "#/c/"
+            + "c/"
             + project.get()
             + "/+/"
             + r.getChange().getId()
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 45294fb..1de9d29 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -433,7 +433,9 @@
       if (notesMigration.commitChangeWrites()) {
         PersonIdent committer = serverIdent.get();
         PersonIdent author =
-            noteUtil.newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
+            noteUtil
+                .getLegacyChangeNoteWrite()
+                .newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
         tr.branch(RefNames.changeMetaRef(c3.getId()))
             .commit()
             .author(author)
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
new file mode 100644
index 0000000..0d24a5d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -0,0 +1,286 @@
+// Copyright (C) 2014 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.acceptance.pgm;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.MoreFiles;
+import com.google.common.io.RecursiveDeleteOption;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Provider;
+import java.nio.file.Files;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Test;
+
+@NoHttpd
+public abstract class AbstractReindexTests extends StandaloneSiteTest {
+  private static final String CHANGES = ChangeSchemaDefinitions.NAME;
+
+  private Project.NameKey project;
+  private String changeId;
+
+  @Test
+  public void reindexFromScratch() throws Exception {
+    setUpChange();
+
+    MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
+    Files.createDirectory(sitePaths.index_dir);
+    assertServerStartupFails();
+
+    runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+    assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      // Query change index
+      assertThat(gApi.changes().query("message:Test").get().stream().map(c -> c.changeId))
+          .containsExactly(changeId);
+      // Query account index
+      assertThat(gApi.accounts().query("admin").get().stream().map(a -> a._accountId))
+          .containsExactly(adminId.get());
+      // Query group index
+      assertThat(
+              gApi.groups()
+                  .query("Group")
+                  .withOption(MEMBERS)
+                  .get()
+                  .stream()
+                  .flatMap(g -> g.members.stream())
+                  .map(a -> a._accountId))
+          .containsExactly(adminId.get());
+      // Query project index
+      assertThat(gApi.projects().query(project.get()).get().stream().map(p -> p.name))
+          .containsExactly(project.get());
+    }
+  }
+
+  @Test
+  public void offlineReindexForChangesIsNotPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "changes",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts shouldn't allow to offline reindex changes")
+        .that(exitCode)
+        .isGreaterThan(0);
+  }
+
+  @Test
+  public void offlineReindexForAccountsIsNotPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "accounts",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts shouldn't allow to offline reindex accounts")
+        .that(exitCode)
+        .isGreaterThan(0);
+  }
+
+  @Test
+  public void offlineReindexForProjectsIsNotPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "projects",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts shouldn't allow to offline reindex projects")
+        .that(exitCode)
+        .isGreaterThan(0);
+  }
+
+  @Test
+  public void offlineReindexForGroupsIsPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "groups",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts should allow to offline reindex groups")
+        .that(exitCode)
+        .isEqualTo(0);
+  }
+
+  @Test
+  public void offlineReindexForAllAvailableIndicesIsPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+
+    assertWithMessage("Slave hosts should allow to perform a general offline reindex")
+        .that(exitCode)
+        .isEqualTo(0);
+  }
+
+  @Test
+  public void onlineUpgradeChanges() throws Exception {
+    int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
+    int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
+
+    // Before storing any changes, switch back to the previous version.
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+    status.setReady(CHANGES, currVersion, false);
+    status.setReady(CHANGES, prevVersion, true);
+    status.save();
+    assertReady(prevVersion);
+
+    setOnlineUpgradeConfig(false);
+    setUpChange();
+    setOnlineUpgradeConfig(true);
+
+    IndexUpgradeController u = new IndexUpgradeController(1);
+    try (ServerContext ctx = startServer(u.module())) {
+      assertSearchVersion(ctx, prevVersion);
+      assertWriteVersions(ctx, prevVersion, currVersion);
+
+      // Updating and searching old schema version works.
+      Provider<InternalChangeQuery> queryProvider =
+          ctx.getInjector().getProvider(InternalChangeQuery.class);
+      assertThat(queryProvider.get().byKey(new Change.Key(changeId))).hasSize(1);
+      assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
+
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      gApi.changes().id(changeId).topic("topic1");
+      assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
+
+      u.runUpgrades();
+      assertThat(u.getStartedAttempts())
+          .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
+      assertThat(u.getSucceededAttempts())
+          .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
+      assertThat(u.getFailedAttempts()).isEmpty();
+
+      assertReady(currVersion);
+      assertSearchVersion(ctx, currVersion);
+      assertWriteVersions(ctx, currVersion);
+
+      // Updating and searching new schema version works.
+      assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
+      assertThat(queryProvider.get().byTopicOpen("topic2")).isEmpty();
+      gApi.changes().id(changeId).topic("topic2");
+      assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
+      assertThat(queryProvider.get().byTopicOpen("topic2")).hasSize(1);
+    }
+  }
+
+  private void setUpChange() throws Exception {
+    project = new Project.NameKey("reindex-project-test");
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      gApi.projects().create(project.get());
+
+      ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
+      in.newBranch = true;
+      changeId = gApi.changes().create(in).info().changeId;
+    }
+  }
+
+  private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+    updateConfig(cfg -> cfg.setBoolean("index", null, "onlineUpgrade", enable));
+  }
+
+  private void enableSlaveMode() throws Exception {
+    updateConfig(config -> config.setBoolean("container", null, "slave", true));
+  }
+
+  private void updateConfig(Consumer<Config> configConsumer) throws Exception {
+    FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+    cfg.load();
+    configConsumer.accept(cfg);
+    cfg.save();
+  }
+
+  private static int runGerritAndReturnExitCode(String... args) throws Exception {
+    return GerritLauncher.mainImpl(args);
+  }
+
+  private void assertSearchVersion(ServerContext ctx, int expected) {
+    assertThat(
+            ctx.getInjector()
+                .getInstance(ChangeIndexCollection.class)
+                .getSearchIndex()
+                .getSchema()
+                .getVersion())
+        .named("search version")
+        .isEqualTo(expected);
+  }
+
+  private void assertWriteVersions(ServerContext ctx, Integer... expected) {
+    assertThat(
+            ctx.getInjector()
+                .getInstance(ChangeIndexCollection.class)
+                .getWriteIndexes()
+                .stream()
+                .map(i -> i.getSchema().getVersion()))
+        .named("write versions")
+        .containsExactlyElementsIn(ImmutableSet.copyOf(expected));
+  }
+
+  private void assertReady(int expectedReady) throws Exception {
+    Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
+    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
+    assertThat(
+            allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v))))
+        .named("ready state for index versions")
+        .isEqualTo(allVersions.stream().collect(toImmutableMap(v -> v, v -> v == expectedReady)));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index 371696c..a8d5644 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -1,7 +1,10 @@
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
-    srcs = glob(["*IT.java"]),
+    srcs = glob(
+        ["*IT.java"],
+        exclude = ["ElasticReindexIT.java"],
+    ),
     group = "pgm",
     labels = [
         "pgm",
@@ -14,9 +17,27 @@
     ],
 )
 
+acceptance_tests(
+    srcs = ["ElasticReindexIT.java"],
+    group = "elastic",
+    labels = [
+        "elastic",
+        "pgm",
+        "no_windows",
+    ],
+    vm_args = ["-Xmx512m"],
+    deps = [
+        ":util",
+        "//java/com/google/gerrit/server/schema",
+    ],
+)
+
 java_library(
     name = "util",
     testonly = 1,
-    srcs = ["IndexUpgradeController.java"],
+    srcs = [
+        "AbstractReindexTests.java",
+        "IndexUpgradeController.java",
+    ],
     deps = ["//java/com/google/gerrit/acceptance:lib"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
new file mode 100644
index 0000000..1aa8d54
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2014 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.acceptance.pgm;
+
+import org.junit.Ignore;
+
+@Ignore
+public class ElasticReindexIT extends AbstractReindexTests {}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
index 4b6f8b2..18d2628 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -14,270 +14,4 @@
 
 package com.google.gerrit.acceptance.pgm;
 
-import static com.google.common.collect.ImmutableMap.toImmutableMap;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.io.MoreFiles;
-import com.google.common.io.RecursiveDeleteOption;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.StandaloneSiteTest;
-import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.index.GerritIndexStatus;
-import com.google.gerrit.server.index.change.ChangeIndexCollection;
-import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.inject.Provider;
-import java.nio.file.Files;
-import java.util.Set;
-import java.util.function.Consumer;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.junit.Test;
-
-@NoHttpd
-public class ReindexIT extends StandaloneSiteTest {
-  private static final String CHANGES = ChangeSchemaDefinitions.NAME;
-
-  private Project.NameKey project;
-  private String changeId;
-
-  @Test
-  public void reindexFromScratch() throws Exception {
-    setUpChange();
-
-    MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
-    Files.createDirectory(sitePaths.index_dir);
-    assertServerStartupFails();
-
-    runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
-    assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
-
-    try (ServerContext ctx = startServer()) {
-      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
-      // Query change index
-      assertThat(gApi.changes().query("message:Test").get().stream().map(c -> c.changeId))
-          .containsExactly(changeId);
-      // Query account index
-      assertThat(gApi.accounts().query("admin").get().stream().map(a -> a._accountId))
-          .containsExactly(adminId.get());
-      // Query group index
-      assertThat(
-              gApi.groups()
-                  .query("Group")
-                  .withOption(MEMBERS)
-                  .get()
-                  .stream()
-                  .flatMap(g -> g.members.stream())
-                  .map(a -> a._accountId))
-          .containsExactly(adminId.get());
-    }
-  }
-
-  @Test
-  public void offlineReindexForChangesIsNotPossibleInSlaveMode() throws Exception {
-    enableSlaveMode();
-
-    int exitCode =
-        runGerritAndReturnExitCode(
-            "reindex",
-            "--index",
-            "changes",
-            "-d",
-            sitePaths.site_path.toString(),
-            "--show-stack-trace");
-
-    assertWithMessage("Slave hosts shouldn't allow to offline reindex changes")
-        .that(exitCode)
-        .isGreaterThan(0);
-  }
-
-  @Test
-  public void offlineReindexForAccountsIsNotPossibleInSlaveMode() throws Exception {
-    enableSlaveMode();
-
-    int exitCode =
-        runGerritAndReturnExitCode(
-            "reindex",
-            "--index",
-            "accounts",
-            "-d",
-            sitePaths.site_path.toString(),
-            "--show-stack-trace");
-
-    assertWithMessage("Slave hosts shouldn't allow to offline reindex accounts")
-        .that(exitCode)
-        .isGreaterThan(0);
-  }
-
-  @Test
-  public void offlineReindexForProjectsIsNotPossibleInSlaveMode() throws Exception {
-    enableSlaveMode();
-
-    int exitCode =
-        runGerritAndReturnExitCode(
-            "reindex",
-            "--index",
-            "projects",
-            "-d",
-            sitePaths.site_path.toString(),
-            "--show-stack-trace");
-
-    assertWithMessage("Slave hosts shouldn't allow to offline reindex projects")
-        .that(exitCode)
-        .isGreaterThan(0);
-  }
-
-  @Test
-  public void offlineReindexForGroupsIsPossibleInSlaveMode() throws Exception {
-    enableSlaveMode();
-
-    int exitCode =
-        runGerritAndReturnExitCode(
-            "reindex",
-            "--index",
-            "groups",
-            "-d",
-            sitePaths.site_path.toString(),
-            "--show-stack-trace");
-
-    assertWithMessage("Slave hosts should allow to offline reindex groups")
-        .that(exitCode)
-        .isEqualTo(0);
-  }
-
-  @Test
-  public void offlineReindexForAllAvailableIndicesIsPossibleInSlaveMode() throws Exception {
-    enableSlaveMode();
-
-    int exitCode =
-        runGerritAndReturnExitCode(
-            "reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
-
-    assertWithMessage("Slave hosts should allow to perform a general offline reindex")
-        .that(exitCode)
-        .isEqualTo(0);
-  }
-
-  @Test
-  public void onlineUpgradeChanges() throws Exception {
-    int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
-    int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
-
-    // Before storing any changes, switch back to the previous version.
-    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
-    status.setReady(CHANGES, currVersion, false);
-    status.setReady(CHANGES, prevVersion, true);
-    status.save();
-    assertReady(prevVersion);
-
-    setOnlineUpgradeConfig(false);
-    setUpChange();
-    setOnlineUpgradeConfig(true);
-
-    IndexUpgradeController u = new IndexUpgradeController(1);
-    try (ServerContext ctx = startServer(u.module())) {
-      assertSearchVersion(ctx, prevVersion);
-      assertWriteVersions(ctx, prevVersion, currVersion);
-
-      // Updating and searching old schema version works.
-      Provider<InternalChangeQuery> queryProvider =
-          ctx.getInjector().getProvider(InternalChangeQuery.class);
-      assertThat(queryProvider.get().byKey(new Change.Key(changeId))).hasSize(1);
-      assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
-
-      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
-      gApi.changes().id(changeId).topic("topic1");
-      assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
-
-      u.runUpgrades();
-      assertThat(u.getStartedAttempts())
-          .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
-      assertThat(u.getSucceededAttempts())
-          .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion));
-      assertThat(u.getFailedAttempts()).isEmpty();
-
-      assertReady(currVersion);
-      assertSearchVersion(ctx, currVersion);
-      assertWriteVersions(ctx, currVersion);
-
-      // Updating and searching new schema version works.
-      assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1);
-      assertThat(queryProvider.get().byTopicOpen("topic2")).isEmpty();
-      gApi.changes().id(changeId).topic("topic2");
-      assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty();
-      assertThat(queryProvider.get().byTopicOpen("topic2")).hasSize(1);
-    }
-  }
-
-  private void setUpChange() throws Exception {
-    project = new Project.NameKey("project");
-    try (ServerContext ctx = startServer()) {
-      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
-      gApi.projects().create(project.get());
-
-      ChangeInput in = new ChangeInput(project.get(), "master", "Test change");
-      in.newBranch = true;
-      changeId = gApi.changes().create(in).info().changeId;
-    }
-  }
-
-  private void setOnlineUpgradeConfig(boolean enable) throws Exception {
-    updateConfig(cfg -> cfg.setBoolean("index", null, "onlineUpgrade", enable));
-  }
-
-  private void enableSlaveMode() throws Exception {
-    updateConfig(config -> config.setBoolean("container", null, "slave", true));
-  }
-
-  private void updateConfig(Consumer<Config> configConsumer) throws Exception {
-    FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
-    cfg.load();
-    configConsumer.accept(cfg);
-    cfg.save();
-  }
-
-  private static int runGerritAndReturnExitCode(String... args) throws Exception {
-    return GerritLauncher.mainImpl(args);
-  }
-
-  private void assertSearchVersion(ServerContext ctx, int expected) {
-    assertThat(
-            ctx.getInjector()
-                .getInstance(ChangeIndexCollection.class)
-                .getSearchIndex()
-                .getSchema()
-                .getVersion())
-        .named("search version")
-        .isEqualTo(expected);
-  }
-
-  private void assertWriteVersions(ServerContext ctx, Integer... expected) {
-    assertThat(
-            ctx.getInjector()
-                .getInstance(ChangeIndexCollection.class)
-                .getWriteIndexes()
-                .stream()
-                .map(i -> i.getSchema().getVersion()))
-        .named("write versions")
-        .containsExactlyElementsIn(ImmutableSet.copyOf(expected));
-  }
-
-  private void assertReady(int expectedReady) throws Exception {
-    Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
-    GerritIndexStatus status = new GerritIndexStatus(sitePaths);
-    assertThat(
-            allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v))))
-        .named("ready state for index versions")
-        .isEqualTo(allVersions.stream().collect(toImmutableMap(v -> v, v -> v == expectedReady)));
-  }
-}
+public class ReindexIT extends AbstractReindexTests {}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index 9edafb8..bc84593 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -235,4 +235,9 @@
     assertThat(persistedWatchedProjects).doesNotContain(pwi);
     assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
   }
+
+  @Test
+  public void postWithoutBody() throws Exception {
+    adminRestSession.post("/accounts/" + admin.username + "/watched.projects").assertOK();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index f89f2a1..171babd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -105,7 +105,9 @@
   public void revisionActionsTwoChangesInTopic() throws Exception {
     String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
-    String changeId2 = createChangeWithTopic().getChangeId();
+    PushOneCommit.Result change2 = createChangeWithTopic();
+    int legacyId2 = change2.getChange().getId().get();
+    String changeId2 = change2.getChangeId();
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
     if (isSubmitWholeTopicEnabled()) {
@@ -113,7 +115,7 @@
       assertThat(info.enabled).isNull();
       assertThat(info.label).isEqualTo("Submit whole topic");
       assertThat(info.method).isEqualTo("POST");
-      assertThat(info.title).isEqualTo("This change depends on other changes which are not ready");
+      assertThat(info.title).matches("Change " + legacyId2 + " is not ready: needs Code-Review");
     } else {
       noSubmitWholeTopicAssertions(actions, 1);
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index e510e25..c723082 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -117,6 +117,13 @@
   }
 
   @Test
+  public void createNewChange_InvalidCommentInCommitMessage() throws Exception {
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "#12345 Test";
+    assertCreateFails(ci, BadRequestException.class, "commit message must be non-empty");
+  }
+
+  @Test
   public void createNewChange() throws Exception {
     ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
     assertThat(info.revisions.get(info.currentRevision).commit.message)
@@ -124,6 +131,15 @@
   }
 
   @Test
+  public void createNewChangeWithCommentsInCommitMessage() throws Exception {
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject += "\n# Comment line";
+    ChangeInfo info = gApi.changes().create(ci).get();
+    assertThat(info.revisions.get(info.currentRevision).commit.message)
+        .doesNotContain("# Comment line");
+  }
+
+  @Test
   public void createNewChangeWithChangeId() throws Exception {
     ChangeInput ci = newChangeInput(ChangeStatus.NEW);
     String changeId = "I1234000000000000000000000000000000000000";
@@ -273,7 +289,9 @@
       assertThat(commit.getShortMessage()).isEqualTo("Create change");
 
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id), c.created, serverIdent.get());
+          changeNoteUtil
+              .getLegacyChangeNoteWrite()
+              .newIdent(getAccount(admin.id), c.created, serverIdent.get());
       assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
 
       assertThat(commit.getCommitterIdent())
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 206b06c..6c779f5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -64,9 +64,8 @@
 
   // download
   @GerritConfig(
-    name = "download.archive",
-    values = {"tar", "tbz2", "tgz", "txz"}
-  )
+      name = "download.archive",
+      values = {"tar", "tbz2", "tgz", "txz"})
 
   // gerrit
   @GerritConfig(name = "gerrit.allProjects", value = "Root")
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index c174583..023c540 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -394,9 +394,8 @@
   @SuppressWarnings("deprecation")
   @Test
   @GerritConfig(
-    name = "repository.testinheritedsubmittype/*.defaultSubmitType",
-    value = "CHERRY_PICK"
-  )
+      name = "repository.testinheritedsubmittype/*.defaultSubmitType",
+      value = "CHERRY_PICK")
   public void repositoryConfigTakesPrecedenceOverInheritedSubmitType() throws Exception {
     // Can't use name() since we need to specify this project name in gerrit.config prior to
     // startup. Pick something reasonably unique instead.
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index e53b997..9621c16 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -950,7 +950,7 @@
   @Test
   public void jsonCommentHasLegacyFormatFalse() throws Exception {
     assume().that(notesMigration.readChanges()).isTrue();
-    assertThat(noteUtil.getWriteJson()).isTrue();
+    assertThat(noteUtil.getChangeNoteJson().getWriteJson()).isTrue();
 
     PushOneCommit.Result result = createChange();
     Change.Id changeId = result.getChange().getId();
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 29c043a..ea44bd7 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -902,7 +902,9 @@
     }
     PersonIdent committer = serverIdent.get();
     PersonIdent author =
-        noteUtil.newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
+        noteUtil
+            .getLegacyChangeNoteWrite()
+            .newIdent(getAccount(admin.getId()), committer.getWhen(), committer);
     serverSideTestRepo
         .branch(RefNames.changeMetaRef(id))
         .commit()
diff --git a/javatests/com/google/gerrit/acceptance/server/change/LegacyCommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/LegacyCommentsIT.java
index a3a0339..e029e7a 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/LegacyCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/LegacyCommentsIT.java
@@ -53,7 +53,7 @@
   @Test
   public void legacyCommentHasLegacyFormatTrue() throws Exception {
     assume().that(notesMigration.readChanges()).isTrue();
-    assertThat(noteUtil.getWriteJson()).isFalse();
+    assertThat(noteUtil.getChangeNoteJson().getWriteJson()).isFalse();
 
     PushOneCommit.Result result = createChange();
     Change.Id changeId = result.getChange().getId();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
index df4076d..438954c 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -47,9 +47,8 @@
   @Test
   @GerritConfig(name = "receiveemail.filter.mode", value = "WHITELIST")
   @GerritConfig(
-    name = "receiveemail.filter.patterns",
-    values = {".+ser@example\\.com", "a@b\\.com"}
-  )
+      name = "receiveemail.filter.patterns",
+      values = {".+ser@example\\.com", "a@b\\.com"})
   public void listFilterWhitelistDoesNotFilterListedUser() throws Exception {
     ChangeInfo changeInfo = createChangeAndReplyByEmail();
     // Check that the comments from the email have been persisted
@@ -60,9 +59,8 @@
   @Test
   @GerritConfig(name = "receiveemail.filter.mode", value = "WHITELIST")
   @GerritConfig(
-    name = "receiveemail.filter.patterns",
-    values = {".+@gerritcodereview\\.com", "a@b\\.com"}
-  )
+      name = "receiveemail.filter.patterns",
+      values = {".+@gerritcodereview\\.com", "a@b\\.com"})
   public void listFilterWhitelistFiltersNotListedUser() throws Exception {
     ChangeInfo changeInfo = createChangeAndReplyByEmail();
     // Check that the comments from the email have NOT been persisted
@@ -76,9 +74,8 @@
   @Test
   @GerritConfig(name = "receiveemail.filter.mode", value = "BLACKLIST")
   @GerritConfig(
-    name = "receiveemail.filter.patterns",
-    values = {".+@gerritcodereview\\.com", "a@b\\.com"}
-  )
+      name = "receiveemail.filter.patterns",
+      values = {".+@gerritcodereview\\.com", "a@b\\.com"})
   public void listFilterBlacklistDoesNotFilterNotListedUser() throws Exception {
     ChangeInfo changeInfo = createChangeAndReplyByEmail();
     // Check that the comments from the email have been persisted
@@ -89,9 +86,8 @@
   @Test
   @GerritConfig(name = "receiveemail.filter.mode", value = "BLACKLIST")
   @GerritConfig(
-    name = "receiveemail.filter.patterns",
-    values = {".+@example\\.com", "a@b\\.com"}
-  )
+      name = "receiveemail.filter.patterns",
+      values = {".+@example\\.com", "a@b\\.com"})
   public void listFilterBlacklistFiltersListedUser() throws Exception {
     ChangeInfo changeInfo = createChangeAndReplyByEmail();
     // Check that the comments from the email have been persisted
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 8ed7bbf..c45ea99 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -64,7 +64,7 @@
   public static Config defaultConfig() {
     Config cfg = new Config();
     // Avoid spurious timeouts during intentional retries due to overloaded test machines.
-    cfg.setString("noteDb", null, "retryTimeout", Integer.MAX_VALUE + "s");
+    cfg.setString("retry", null, "timeout", Integer.MAX_VALUE + "s");
     return cfg;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
index 95d96b5..b7ce7bc 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -17,8 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.formatTime;
 import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
+import static com.google.gerrit.server.notedb.NoteDbUtil.formatTime;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 9a5a899..5694bd0 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
@@ -31,13 +32,11 @@
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @NoHttpd
 @UseSsh
 public class SshCommandsIT extends AbstractDaemonTest {
-  private static final Logger log = LoggerFactory.getLogger(SshCommandsIT.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   // TODO: It would be better to dynamically generate these lists
   private static final List<String> COMMON_ROOT_COMMANDS =
@@ -129,7 +128,7 @@
         // content of the stderr, which will always start with "gerrit command" when the --help
         // option is used.
         String cmd = String.format("gerrit%s%s %s", root.isEmpty() ? "" : " ", root, command);
-        log.debug(cmd);
+        logger.atFine().log(cmd);
         adminSshSession.exec(String.format("%s --help", cmd));
         String response = adminSshSession.getError();
         assertWithMessage(String.format("command %s failed: %s", command, response))
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index 95c585d..1721545 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -56,9 +56,8 @@
 
   @Test
   @GerritConfig(
-    name = "download.archive",
-    values = {"tar", "tbz2", "tgz", "txz"}
-  )
+      name = "download.archive",
+      values = {"tar", "tbz2", "tgz", "txz"})
   public void zipFormatDisabled() throws Exception {
     assertArchiveNotPermitted();
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index a2f5229..1249909 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -3,7 +3,11 @@
 java_library(
     name = "elasticsearch_test_utils",
     testonly = 1,
-    srcs = ["ElasticTestUtils.java"],
+    srcs = [
+        "ElasticContainer.java",
+        "ElasticTestUtils.java",
+    ],
+    visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/elasticsearch",
         "//java/com/google/gerrit/extensions:api",
@@ -14,9 +18,11 @@
         "//lib:gson",
         "//lib:guava",
         "//lib:junit",
-        "//lib/elasticsearch",
+        "//lib/guice",
+        "//lib/httpcomponents:httpcore",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/testcontainers",
         "//lib/truth",
     ],
 )
@@ -33,6 +39,7 @@
     size = "large",
     srcs = [src],
     tags = [
+        "docker",
         "elastic",
     ],
     deps = [
@@ -43,7 +50,9 @@
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//javatests/com/google/gerrit/server/query/%s:abstract_query_tests" % name,
         "//lib/guice",
+        "//lib/httpcomponents:httpcore",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/testcontainers",
     ],
 ) for name, src in ELASTICSEARCH_TESTS.items()]
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
new file mode 100644
index 0000000..c78f7c0
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -0,0 +1,75 @@
+// 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.elasticsearch;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.apache.http.HttpHost;
+import org.junit.AssumptionViolatedException;
+import org.testcontainers.containers.GenericContainer;
+
+/* Helper class for running ES integration tests in docker container */
+public class ElasticContainer<SELF extends ElasticContainer<SELF>> extends GenericContainer<SELF> {
+  private static final int ELASTICSEARCH_DEFAULT_PORT = 9200;
+
+  public static ElasticContainer<?> createAndStart(ElasticVersion version) {
+    // Assumption violation is not natively supported by Testcontainers.
+    // See https://github.com/testcontainers/testcontainers-java/issues/343
+    try {
+      ElasticContainer<?> container = new ElasticContainer<>(version);
+      container.start();
+      return container;
+    } catch (Throwable t) {
+      throw new AssumptionViolatedException("Unable to start container", t);
+    }
+  }
+
+  public static ElasticContainer<?> createAndStart() {
+    return createAndStart(ElasticVersion.V2_4);
+  }
+
+  private static String getImageName(ElasticVersion version) {
+    switch (version) {
+      case V2_4:
+        return "elasticsearch:2.4.6-alpine";
+      case V5_6:
+        return "elasticsearch:5.6.9-alpine";
+      case V6_2:
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4";
+    }
+    throw new IllegalStateException("No tests for version: " + version.name());
+  }
+
+  private ElasticContainer(ElasticVersion version) {
+    super(getImageName(version));
+  }
+
+  @Override
+  protected void configure() {
+    addExposedPort(ELASTICSEARCH_DEFAULT_PORT);
+
+    // https://github.com/docker-library/elasticsearch/issues/58
+    addEnv("-Ees.network.host", "0.0.0.0");
+  }
+
+  @Override
+  protected Set<Integer> getLivenessCheckPorts() {
+    return ImmutableSet.of(getMappedPort(ELASTICSEARCH_DEFAULT_PORT));
+  }
+
+  public HttpHost getHttpHost() {
+    return new HttpHost(getContainerIpAddress(), getMappedPort(ELASTICSEARCH_DEFAULT_PORT));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
index 6cfc583..4f0f8b0 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
@@ -21,9 +21,7 @@
 import com.google.gerrit.testing.IndexConfig;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.lib.Config;
-import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
@@ -34,22 +32,23 @@
   }
 
   private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
 
   @BeforeClass
-  public static void startIndexService() throws InterruptedException, ExecutionException {
+  public static void startIndexService() {
     if (nodeInfo != null) {
       // do not start Elasticsearch twice
       return;
     }
-    nodeInfo = ElasticTestUtils.startElasticsearchNode();
+
+    container = ElasticContainer.createAndStart();
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
   public static void stopElasticsearchServer() {
-    if (nodeInfo != null) {
-      nodeInfo.node.close();
-      nodeInfo.elasticDir.delete();
-      nodeInfo = null;
+    if (container != null) {
+      container.stop();
     }
   }
 
@@ -57,11 +56,10 @@
     return testName.getMethodName().toLowerCase() + "_";
   }
 
-  @After
-  public void cleanupIndex() {
-    if (nodeInfo != null) {
-      ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
-    }
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
   }
 
   @Override
@@ -70,7 +68,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
index 5949c06..a02d691 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
@@ -18,17 +18,12 @@
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
 import com.google.gerrit.testing.IndexConfig;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
-import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
-import org.junit.Test;
 
 public class ElasticQueryChangesTest extends AbstractQueryChangesTest {
   @ConfigSuite.Default
@@ -37,22 +32,23 @@
   }
 
   private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
 
   @BeforeClass
-  public static void startIndexService() throws InterruptedException, ExecutionException {
+  public static void startIndexService() {
     if (nodeInfo != null) {
       // do not start Elasticsearch twice
       return;
     }
-    nodeInfo = ElasticTestUtils.startElasticsearchNode();
+
+    container = ElasticContainer.createAndStart();
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
   public static void stopElasticsearchServer() {
-    if (nodeInfo != null) {
-      nodeInfo.node.close();
-      nodeInfo.elasticDir.delete();
-      nodeInfo = null;
+    if (container != null) {
+      container.stop();
     }
   }
 
@@ -60,11 +56,10 @@
     return testName.getMethodName().toLowerCase() + "_";
   }
 
-  @After
-  public void cleanupIndex() {
-    if (nodeInfo != null) {
-      ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
-    }
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
   }
 
   @Override
@@ -73,15 +68,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
-
-  @Test
-  public void byOwnerInvalidQuery() throws Exception {
-    TestRepository<Repo> repo = createProject("repo");
-    insert(repo, newChange(repo), userId);
-    String nameEmail = user.asIdentifiedUser().getNameEmail();
-    assertQuery("owner: \"" + nameEmail + "\"\\");
-  }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
index a1c331d..f13c491 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
@@ -21,9 +21,7 @@
 import com.google.gerrit.testing.IndexConfig;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.lib.Config;
-import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
@@ -34,22 +32,23 @@
   }
 
   private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
 
   @BeforeClass
-  public static void startIndexService() throws InterruptedException, ExecutionException {
+  public static void startIndexService() {
     if (nodeInfo != null) {
       // do not start Elasticsearch twice
       return;
     }
-    nodeInfo = ElasticTestUtils.startElasticsearchNode();
+
+    container = ElasticContainer.createAndStart();
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
   public static void stopElasticsearchServer() {
-    if (nodeInfo != null) {
-      nodeInfo.node.close();
-      nodeInfo.elasticDir.delete();
-      nodeInfo = null;
+    if (container != null) {
+      container.stop();
     }
   }
 
@@ -57,11 +56,10 @@
     return testName.getMethodName().toLowerCase() + "_";
   }
 
-  @After
-  public void cleanupIndex() {
-    if (nodeInfo != null) {
-      ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
-    }
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
   }
 
   @Override
@@ -70,7 +68,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
index 07fbf56..dd04010 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
@@ -21,9 +21,7 @@
 import com.google.gerrit.testing.IndexConfig;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.lib.Config;
-import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
@@ -34,22 +32,23 @@
   }
 
   private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
 
   @BeforeClass
-  public static void startIndexService() throws InterruptedException, ExecutionException {
+  public static void startIndexService() {
     if (nodeInfo != null) {
       // do not start Elasticsearch twice
       return;
     }
-    nodeInfo = ElasticTestUtils.startElasticsearchNode();
+
+    container = ElasticContainer.createAndStart();
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
   public static void stopElasticsearchServer() {
-    if (nodeInfo != null) {
-      nodeInfo.node.close();
-      nodeInfo.elasticDir.delete();
-      nodeInfo = null;
+    if (container != null) {
+      container.stop();
     }
   }
 
@@ -57,11 +56,10 @@
     return testName.getMethodName().toLowerCase() + "_";
   }
 
-  @After
-  public void cleanupIndex() {
-    if (nodeInfo != null) {
-      ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
-    }
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
   }
 
   @Override
@@ -70,7 +68,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
-    ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index ed21e6e..ca52e2a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -14,226 +14,39 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.elasticsearch.ElasticAccountIndex.ACCOUNTS;
-import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CHANGES;
-import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CLOSED_CHANGES;
-import static com.google.gerrit.elasticsearch.ElasticChangeIndex.OPEN_CHANGES;
-import static com.google.gerrit.elasticsearch.ElasticGroupIndex.GROUPS;
-import static com.google.gerrit.elasticsearch.ElasticProjectIndex.PROJECTS;
-
-import com.google.common.base.Strings;
-import com.google.common.io.Files;
-import com.google.gerrit.elasticsearch.ElasticAccountIndex.AccountMapping;
-import com.google.gerrit.elasticsearch.ElasticChangeIndex.ChangeMapping;
-import com.google.gerrit.elasticsearch.ElasticGroupIndex.GroupMapping;
-import com.google.gerrit.elasticsearch.ElasticProjectIndex.ProjectMapping;
-import com.google.gerrit.index.Schema;
-import com.google.gerrit.index.project.ProjectData;
-import com.google.gerrit.index.project.ProjectSchemaDefinitions;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.server.index.IndexModule.IndexType;
-import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
-import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import java.io.File;
-import java.nio.file.Path;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import java.io.IOException;
+import java.util.Collection;
 import org.eclipse.jgit.lib.Config;
-import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.node.Node;
-import org.elasticsearch.node.NodeBuilder;
 
-final class ElasticTestUtils {
-  static final Gson gson =
-      new GsonBuilder()
-          .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
-          .create();
+public final class ElasticTestUtils {
+  public static class ElasticNodeInfo {
+    public final int port;
 
-  static class ElasticNodeInfo {
-    final Node node;
-    final String port;
-    final File elasticDir;
-
-    private ElasticNodeInfo(Node node, File rootDir, String port) {
-      this.node = node;
+    public ElasticNodeInfo(int port) {
       this.port = port;
-      this.elasticDir = rootDir;
     }
   }
 
-  static void configure(Config config, String port, String prefix) {
+  public static void configure(Config config, int port, String prefix) {
     config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
     config.setString("elasticsearch", "test", "protocol", "http");
     config.setString("elasticsearch", "test", "hostname", "localhost");
-    config.setString("elasticsearch", "test", "port", port);
+    config.setInt("elasticsearch", "test", "port", port);
     config.setString("elasticsearch", null, "prefix", prefix);
+    config.setString("index", null, "maxLimit", "10000");
   }
 
-  static ElasticNodeInfo startElasticsearchNode() throws InterruptedException, ExecutionException {
-    File elasticDir = Files.createTempDir();
-    Path elasticDirPath = elasticDir.toPath();
-    Settings settings =
-        Settings.settingsBuilder()
-            .put("cluster.name", "gerrit")
-            .put("node.name", "Gerrit Elasticsearch Test Node")
-            .put("node.local", true)
-            .put("discovery.zen.ping.multicast.enabled", false)
-            .put("index.store.fs.memory.enabled", true)
-            .put("index.gateway.type", "none")
-            .put("index.max_result_window", Integer.MAX_VALUE)
-            .put("gateway.type", "default")
-            .put("http.port", 0)
-            .put("discovery.zen.ping.unicast.hosts", "[\"localhost\"]")
-            .put("path.home", elasticDirPath.toAbsolutePath())
-            .put("path.data", elasticDirPath.resolve("data").toAbsolutePath())
-            .put("path.work", elasticDirPath.resolve("work").toAbsolutePath())
-            .put("path.logs", elasticDirPath.resolve("logs").toAbsolutePath())
-            .put("transport.tcp.connect_timeout", "60s")
-            .build();
-
-    // Start the node
-    Node node = NodeBuilder.nodeBuilder().settings(settings).node();
-
-    // Wait for it to be ready
-    node.client().admin().cluster().prepareHealth().setWaitForYellowStatus().execute().actionGet();
-
-    assertThat(node.isClosed()).isFalse();
-    return new ElasticNodeInfo(node, elasticDir, getHttpPort(node));
-  }
-
-  static void deleteAllIndexes(ElasticNodeInfo nodeInfo, String prefix) {
-    Schema<ChangeData> changeSchema = ChangeSchemaDefinitions.INSTANCE.getLatest();
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareDelete(String.format("%s%s_%04d", prefix, CHANGES, changeSchema.getVersion()))
-        .execute()
-        .actionGet();
-
-    Schema<AccountState> accountSchema = AccountSchemaDefinitions.INSTANCE.getLatest();
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareDelete(String.format("%s%s_%04d", prefix, ACCOUNTS, accountSchema.getVersion()))
-        .execute()
-        .actionGet();
-
-    Schema<InternalGroup> groupSchema = GroupSchemaDefinitions.INSTANCE.getLatest();
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareDelete(String.format("%s%s_%04d", prefix, GROUPS, groupSchema.getVersion()))
-        .execute()
-        .actionGet();
-
-    Schema<ProjectData> projectSchema = ProjectSchemaDefinitions.INSTANCE.getLatest();
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareDelete(String.format("%s%s_%04d", prefix, PROJECTS, projectSchema.getVersion()))
-        .execute()
-        .actionGet();
-  }
-
-  static class NodeInfo {
-    String httpAddress;
-  }
-
-  static class Info {
-    Map<String, NodeInfo> nodes;
-  }
-
-  static void createAllIndexes(ElasticNodeInfo nodeInfo, String prefix) {
-    Schema<ChangeData> changeSchema = ChangeSchemaDefinitions.INSTANCE.getLatest();
-    ChangeMapping openChangesMapping = new ChangeMapping(changeSchema);
-    ChangeMapping closedChangesMapping = new ChangeMapping(changeSchema);
-    openChangesMapping.closedChanges = null;
-    closedChangesMapping.openChanges = null;
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareCreate(String.format("%s%s_%04d", prefix, CHANGES, changeSchema.getVersion()))
-        .addMapping(OPEN_CHANGES, gson.toJson(openChangesMapping))
-        .addMapping(CLOSED_CHANGES, gson.toJson(closedChangesMapping))
-        .execute()
-        .actionGet();
-
-    Schema<AccountState> accountSchema = AccountSchemaDefinitions.INSTANCE.getLatest();
-    AccountMapping accountMapping = new AccountMapping(accountSchema);
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareCreate(String.format("%s%s_%04d", prefix, ACCOUNTS, accountSchema.getVersion()))
-        .addMapping(ElasticAccountIndex.ACCOUNTS, gson.toJson(accountMapping))
-        .execute()
-        .actionGet();
-
-    Schema<InternalGroup> groupSchema = GroupSchemaDefinitions.INSTANCE.getLatest();
-    GroupMapping groupMapping = new GroupMapping(groupSchema);
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareCreate(String.format("%s%s_%04d", prefix, GROUPS, groupSchema.getVersion()))
-        .addMapping(ElasticGroupIndex.GROUPS, gson.toJson(groupMapping))
-        .execute()
-        .actionGet();
-
-    Schema<ProjectData> projectSchema = ProjectSchemaDefinitions.INSTANCE.getLatest();
-    ProjectMapping projectMapping = new ProjectMapping(projectSchema);
-    nodeInfo
-        .node
-        .client()
-        .admin()
-        .indices()
-        .prepareCreate(String.format("%s%s_%04d", prefix, PROJECTS, projectSchema.getVersion()))
-        .addMapping(ElasticProjectIndex.PROJECTS, gson.toJson(projectMapping))
-        .execute()
-        .actionGet();
-  }
-
-  private static String getHttpPort(Node node) throws InterruptedException, ExecutionException {
-    String nodes =
-        node.client().admin().cluster().nodesInfo(new NodesInfoRequest("*")).get().toString();
-    Gson gson =
-        new GsonBuilder()
-            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
-            .create();
-    Info info = gson.fromJson(nodes, Info.class);
-    if (info.nodes == null || info.nodes.size() != 1) {
-      throw new RuntimeException("Cannot extract local Elasticsearch http port");
+  public static void createAllIndexes(Injector injector) throws IOException {
+    Collection<IndexDefinition<?, ?, ?>> indexDefs =
+        injector.getInstance(Key.get(new TypeLiteral<Collection<IndexDefinition<?, ?, ?>>>() {}));
+    for (IndexDefinition<?, ?, ?> indexDef : indexDefs) {
+      indexDef.getIndexCollection().getSearchIndex().deleteAll();
     }
-    Iterator<NodeInfo> values = info.nodes.values().iterator();
-    String httpAddress = values.next().httpAddress;
-    if (Strings.isNullOrEmpty(httpAddress)) {
-      throw new RuntimeException("Cannot extract local Elasticsearch http port");
-    }
-    if (httpAddress.indexOf(':') < 0) {
-      throw new RuntimeException("Seems that port is not included in Elasticsearch http_address");
-    }
-    return httpAddress.substring(httpAddress.indexOf(':') + 1, httpAddress.length());
   }
 
   private ElasticTestUtils() {
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
new file mode 100644
index 0000000..649fc6f
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV5QueryAccountsTest extends AbstractQueryAccountsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
new file mode 100644
index 0000000..4aa08fa
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV5QueryChangesTest extends AbstractQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
new file mode 100644
index 0000000..72f8b49
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV5QueryGroupsTest extends AbstractQueryGroupsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
new file mode 100644
index 0000000..7b49e1d
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV5QueryProjectsTest extends AbstractQueryProjectsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
new file mode 100644
index 0000000..db710f6
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV6QueryAccountsTest extends AbstractQueryAccountsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_2);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
new file mode 100644
index 0000000..043de4e
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV6QueryChangesTest extends AbstractQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_2);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
new file mode 100644
index 0000000..b126c9d
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV6QueryGroupsTest extends AbstractQueryGroupsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_2);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
new file mode 100644
index 0000000..eaaf0c8
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -0,0 +1,73 @@
+// 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.elasticsearch;
+
+import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class ElasticV6QueryProjectsTest extends AbstractQueryProjectsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return IndexConfig.createForElasticsearch();
+  }
+
+  private static ElasticNodeInfo nodeInfo;
+  private static ElasticContainer<?> container;
+
+  @BeforeClass
+  public static void startIndexService() {
+    if (nodeInfo != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_2);
+    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  private String testName() {
+    return testName.getMethodName().toLowerCase() + "_";
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    String indicesPrefix = testName();
+    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
new file mode 100644
index 0000000..e0da86a
--- /dev/null
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -0,0 +1,45 @@
+// 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.elasticsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class ElasticVersionTest {
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void supportedVersion() throws Exception {
+    assertThat(ElasticVersion.forVersion("2.4.0")).isEqualTo(ElasticVersion.V2_4);
+    assertThat(ElasticVersion.forVersion("2.4.6")).isEqualTo(ElasticVersion.V2_4);
+
+    assertThat(ElasticVersion.forVersion("5.6.0")).isEqualTo(ElasticVersion.V5_6);
+    assertThat(ElasticVersion.forVersion("5.6.9")).isEqualTo(ElasticVersion.V5_6);
+
+    assertThat(ElasticVersion.forVersion("6.2.0")).isEqualTo(ElasticVersion.V6_2);
+    assertThat(ElasticVersion.forVersion("6.2.4")).isEqualTo(ElasticVersion.V6_2);
+  }
+
+  @Test
+  public void unsupportedVersion() throws Exception {
+    exception.expect(ElasticVersion.InvalidVersion.class);
+    exception.expectMessage(
+        "Invalid version: [4.0.0]. Supported versions: " + ElasticVersion.supportedVersions());
+    ElasticVersion.forVersion("4.0.0");
+  }
+}
diff --git a/javatests/com/google/gerrit/gpg/BUILD b/javatests/com/google/gerrit/gpg/BUILD
index ab66f9a..baf65b7 100644
--- a/javatests/com/google/gerrit/gpg/BUILD
+++ b/javatests/com/google/gerrit/gpg/BUILD
@@ -24,11 +24,11 @@
         "//lib/bouncycastle:bcpg-neverlink",
         "//lib/bouncycastle:bcprov",
         "//lib/bouncycastle:bcprov-neverlink",
+        "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
-        "//lib/log:api",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/BUILD b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
new file mode 100644
index 0000000..98d12b2
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
@@ -0,0 +1,12 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "dropwizard_tests",
+    srcs = glob(["**/*.java"]),
+    tags = ["metrics"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/metrics/dropwizard",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
new file mode 100644
index 0000000..9b21bf6
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
@@ -0,0 +1,44 @@
+// 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.metrics.dropwizard;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class DropWizardMetricMakerTest {
+  DropWizardMetricMaker metrics =
+      new DropWizardMetricMaker(null /* MetricRegistry unused in tests */);
+
+  @Test
+  public void shouldSanitizeUnwantedChars() throws Exception {
+    assertThat(metrics.sanitizeMetricName("very+confusing$long#metric@net/name^1"))
+        .isEqualTo("very_confusing_long_metric_net/name_1");
+    assertThat(metrics.sanitizeMetricName("/metric/submetric")).isEqualTo("_metric/submetric");
+  }
+
+  @Test
+  public void shouldReduceConsecutiveSlashesToOne() throws Exception {
+    assertThat(metrics.sanitizeMetricName("/metric//submetric1///submetric2/submetric3"))
+        .isEqualTo("_metric/submetric1/submetric2/submetric3");
+  }
+
+  @Test
+  public void shouldNotFinishWithSlash() throws Exception {
+    assertThat(metrics.sanitizeMetricName("metric/")).isEqualTo("metric");
+    assertThat(metrics.sanitizeMetricName("metric//")).isEqualTo("metric");
+    assertThat(metrics.sanitizeMetricName("metric/submetric/")).isEqualTo("metric/submetric");
+  }
+}
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 586c065..5e93a09 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -1,6 +1,7 @@
 package com.google.gerrit.server.auth.oauth;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index 278330b..ab88169 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -5,12 +5,16 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/cache/testing",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java b/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java
new file mode 100644
index 0000000..8bf9762
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java
@@ -0,0 +1,116 @@
+// 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.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ProtoCacheSerializersTest {
+  @Test
+  public void objectIdFromByteString() {
+    ObjectIdConverter idConverter = ObjectIdConverter.create();
+    assertThat(
+            idConverter.fromByteString(
+                bytes(
+                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa)))
+        .isEqualTo(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
+    assertThat(
+            idConverter.fromByteString(
+                bytes(
+                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb)))
+        .isEqualTo(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
+  }
+
+  @Test
+  public void objectIdFromByteStringWrongSize() {
+    try {
+      ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo"));
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void objectIdToByteString() {
+    ObjectIdConverter idConverter = ObjectIdConverter.create();
+    assertThat(
+            idConverter.toByteString(
+                ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
+        .isEqualTo(
+            bytes(
+                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa));
+    assertThat(
+            idConverter.toByteString(
+                ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
+        .isEqualTo(
+            bytes(
+                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb));
+  }
+
+  @Test
+  public void parseUncheckedWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
+    try {
+      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedInvalidData() {
+    byte[] bytes = new byte[] {0x00};
+    try {
+      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUnchecked() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
+    assertThat(ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), bytes))
+        .isEqualTo(proto);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 32d0d27..4180192 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -47,7 +47,7 @@
             StringSerializer.INSTANCE,
             version,
             1 << 20,
-            0);
+            null);
     return new H2CacheImpl<>(MoreExecutors.directExecutor(), store, KEY_TYPE, mem);
   }
 
diff --git a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
index 5b77094..03e0d4e 100644
--- a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
+import com.google.gerrit.server.change.ChangeKindCacheImpl.Key;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
@@ -28,7 +30,7 @@
   @Test
   public void keySerializer() throws Exception {
     ChangeKindCacheImpl.Key key =
-        new ChangeKindCacheImpl.Key(
+        Key.create(
             ObjectId.zeroId(),
             ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
             "aStrategy");
@@ -54,7 +56,7 @@
   @Test
   public void keyFields() throws Exception {
     assertThatSerializedClass(ChangeKindCacheImpl.Key.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.of(
                 "prior", ObjectId.class, "next", ObjectId.class, "strategyName", String.class));
   }
diff --git a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
index 69fc531..c8e6f2b 100644
--- a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
diff --git a/javatests/com/google/gerrit/server/config/SitePathsTest.java b/javatests/com/google/gerrit/server/config/SitePathsTest.java
index 058a497..853db27 100644
--- a/javatests/com/google/gerrit/server/config/SitePathsTest.java
+++ b/javatests/com/google/gerrit/server/config/SitePathsTest.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 
-import com.google.gerrit.extensions.common.testing.PathSubject;
 import com.google.gerrit.server.util.HostPlatform;
 import com.google.gerrit.testing.GerritBaseTests;
 import java.io.IOException;
@@ -32,8 +32,8 @@
     final Path root = random();
     final SitePaths site = new SitePaths(root);
     assertThat(site.isNew).isTrue();
-    PathSubject.assertThat(site.site_path).isEqualTo(root);
-    PathSubject.assertThat(site.etc_dir).isEqualTo(root.resolve("etc"));
+    assertThat(site.site_path).isEqualTo(root);
+    assertThat(site.etc_dir).isEqualTo(root.resolve("etc"));
   }
 
   @Test
@@ -44,7 +44,7 @@
 
       final SitePaths site = new SitePaths(root);
       assertThat(site.isNew).isTrue();
-      PathSubject.assertThat(site.site_path).isEqualTo(root);
+      assertThat(site.site_path).isEqualTo(root);
     } finally {
       Files.delete(root);
     }
@@ -60,7 +60,7 @@
 
       final SitePaths site = new SitePaths(root);
       assertThat(site.isNew).isFalse();
-      PathSubject.assertThat(site.site_path).isEqualTo(root);
+      assertThat(site.site_path).isEqualTo(root);
     } finally {
       Files.delete(txt);
       Files.delete(root);
@@ -84,16 +84,15 @@
     final Path root = random();
     final SitePaths site = new SitePaths(root);
 
-    PathSubject.assertThat(site.resolve(null)).isNull();
-    PathSubject.assertThat(site.resolve("")).isNull();
+    assertThat(site.resolve(null)).isNull();
+    assertThat(site.resolve("")).isNull();
 
-    PathSubject.assertThat(site.resolve("a")).isNotNull();
-    PathSubject.assertThat(site.resolve("a"))
-        .isEqualTo(root.resolve("a").toAbsolutePath().normalize());
+    assertThat(site.resolve("a")).isNotNull();
+    assertThat(site.resolve("a")).isEqualTo(root.resolve("a").toAbsolutePath().normalize());
 
     final String pfx = HostPlatform.isWin32() ? "C:/" : "/";
-    PathSubject.assertThat(site.resolve(pfx + "a")).isNotNull();
-    PathSubject.assertThat(site.resolve(pfx + "a")).isEqualTo(Paths.get(pfx + "a"));
+    assertThat(site.resolve(pfx + "a")).isNotNull();
+    assertThat(site.resolve(pfx + "a")).isEqualTo(Paths.get(pfx + "a"));
   }
 
   private static Path random() throws IOException {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index b8f544a..de964d8 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -498,7 +498,11 @@
   private RevCommit writeCommit(String body) throws Exception {
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     return writeCommit(
-        body, noteUtil.newIdent(changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent), false);
+        body,
+        noteUtil
+            .getLegacyChangeNoteWrite()
+            .newIdent(changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent),
+        false);
   }
 
   private RevCommit writeCommit(String body, PersonIdent author) throws Exception {
@@ -509,7 +513,9 @@
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     return writeCommit(
         body,
-        noteUtil.newIdent(changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent),
+        noteUtil
+            .getLegacyChangeNoteWrite()
+            .newIdent(changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent),
         initWorkInProgress);
   }
 
@@ -555,7 +561,9 @@
 
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
     walk.reset();
-    ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
-    return new ChangeNotesParser(newChange().getId(), tip, walk, noteUtil, args.metrics);
+    ChangeNoteJson changeNoteJson = injector.getInstance(ChangeNoteJson.class);
+    LegacyChangeNoteRead reader = injector.getInstance(LegacyChangeNoteRead.class);
+    return new ChangeNotesParser(
+        newChange().getId(), tip, walk, changeNoteJson, reader, args.metrics);
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index c0f2c43..3d65eae 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
+import static com.google.gerrit.server.cache.ProtoCacheSerializers.toByteString;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableList;
@@ -40,7 +41,7 @@
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
@@ -50,7 +51,6 @@
 import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
 import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
 import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.StandardKeyEncoder;
 import com.google.inject.TypeLiteral;
 import com.google.protobuf.ByteString;
@@ -58,7 +58,6 @@
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
@@ -71,7 +70,7 @@
   private static final Change.Id ID = new Change.Id(123);
   private static final ObjectId SHA =
       ObjectId.fromString("1234567812345678123456781234567812345678");
-  private static final ByteString SHA_BYTES = toByteString(SHA);
+  private static final ByteString SHA_BYTES = ObjectIdConverter.create().toByteString(SHA);
   private static final String CHANGE_KEY = "Iabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
 
   private ChangeColumns cols;
@@ -944,14 +943,4 @@
     // additional assertions if necessary.
     return actual;
   }
-
-  private static ByteString toByteString(ObjectId id) {
-    byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-    id.copyRawTo(buf, 0);
-    return ByteString.copyFrom(buf);
-  }
-
-  private <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
-    return ProtoCacheSerializers.toByteString(object, codec);
-  }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 9d38704..74ba0c2 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -77,6 +77,8 @@
 public class ChangeNotesTest extends AbstractChangeNotesTest {
   @Inject private DraftCommentNotes.Factory draftNotesFactory;
 
+  @Inject private ChangeNoteJson changeNoteJson;
+  @Inject private LegacyChangeNoteRead legacyChangeNoteRead;
   @Inject private ChangeNoteUtil noteUtil;
 
   @Inject private @GerritServerId String serverId;
@@ -1195,7 +1197,7 @@
                   + "File: a.txt\n"
                   + "\n"
                   + "1:2-3:4\n"
-                  + ChangeNoteUtil.formatTime(serverIdent, ts)
+                  + NoteDbUtil.formatTime(serverIdent, ts)
                   + "\n"
                   + "Author: Change Owner <1@gerrit>\n"
                   + "Unresolved: false\n"
@@ -1288,7 +1290,13 @@
 
     try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       ChangeNotesParser notesWithComments =
-          new ChangeNotesParser(c.getId(), commitWithComments.copy(), rw, noteUtil, args.metrics);
+          new ChangeNotesParser(
+              c.getId(),
+              commitWithComments.copy(),
+              rw,
+              changeNoteJson,
+              legacyChangeNoteRead,
+              args.metrics);
       ChangeNotesState state = notesWithComments.parseAll();
       assertThat(state.approvals()).isEmpty();
       assertThat(state.publishedComments()).hasSize(1);
@@ -1296,7 +1304,14 @@
 
     try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       ChangeNotesParser notesWithApprovals =
-          new ChangeNotesParser(c.getId(), commitWithApprovals.copy(), rw, noteUtil, args.metrics);
+          new ChangeNotesParser(
+              c.getId(),
+              commitWithApprovals.copy(),
+              rw,
+              changeNoteJson,
+              legacyChangeNoteRead,
+              args.metrics);
+
       ChangeNotesState state = notesWithApprovals.parseAll();
       assertThat(state.approvals()).hasSize(1);
       assertThat(state.publishedComments()).hasSize(1);
@@ -1666,7 +1681,7 @@
                     + "File: file1\n"
                     + "\n"
                     + "1:1-2:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time1)
+                    + NoteDbUtil.formatTime(serverIdent, time1)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Unresolved: false\n"
@@ -1675,7 +1690,7 @@
                     + "comment 1\n"
                     + "\n"
                     + "2:1-3:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time2)
+                    + NoteDbUtil.formatTime(serverIdent, time2)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Unresolved: false\n"
@@ -1686,7 +1701,7 @@
                     + "File: file2\n"
                     + "\n"
                     + "3:0-4:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time3)
+                    + NoteDbUtil.formatTime(serverIdent, time3)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Unresolved: false\n"
@@ -1766,7 +1781,7 @@
                     + "File: file1\n"
                     + "\n"
                     + "1:1-2:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time1)
+                    + NoteDbUtil.formatTime(serverIdent, time1)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Unresolved: false\n"
@@ -1775,7 +1790,7 @@
                     + "comment 1\n"
                     + "\n"
                     + "2:1-3:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time2)
+                    + NoteDbUtil.formatTime(serverIdent, time2)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Unresolved: false\n"
@@ -1854,7 +1869,7 @@
                     + "File: file1\n"
                     + "\n"
                     + "1:1-2:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time1)
+                    + NoteDbUtil.formatTime(serverIdent, time1)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Unresolved: false\n"
@@ -1863,7 +1878,7 @@
                     + "comment 1\n"
                     + "\n"
                     + "1:1-2:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time2)
+                    + NoteDbUtil.formatTime(serverIdent, time2)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Parent: uuid1\n"
@@ -1951,7 +1966,7 @@
 
       byte[] bytes = walk.getObjectReader().open(note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      String timeStr = ChangeNoteUtil.formatTime(serverIdent, time);
+      String timeStr = NoteDbUtil.formatTime(serverIdent, time);
 
       if (!testJson()) {
         assertThat(noteString)
@@ -2048,7 +2063,7 @@
                     + "File: file\n"
                     + "\n"
                     + "1:1-2:1\n"
-                    + ChangeNoteUtil.formatTime(serverIdent, time)
+                    + NoteDbUtil.formatTime(serverIdent, time)
                     + "\n"
                     + "Author: Other Account <2@gerrit>\n"
                     + "Real-author: Change Owner <1@gerrit>\n"
@@ -2103,7 +2118,7 @@
 
       byte[] bytes = walk.getObjectReader().open(note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      String timeStr = ChangeNoteUtil.formatTime(serverIdent, time);
+      String timeStr = NoteDbUtil.formatTime(serverIdent, time);
 
       if (!testJson()) {
         assertThat(noteString)
@@ -3504,14 +3519,14 @@
   public void setRevertOfOnChildCommitFails() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setRevertOf(newChange().getId().get());
     exception.expect(OrmException.class);
     exception.expectMessage("Given ChangeUpdate is only allowed on initial commit");
-    update.setRevertOf(newChange().getId().get());
     update.commit();
   }
 
   private boolean testJson() {
-    return noteUtil.getWriteJson();
+    return noteUtil.getChangeNoteJson().getWriteJson();
   }
 
   private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index aa37d51..e7d8956 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -68,7 +68,7 @@
 
     // Match ChangeNoteUtil#gson as of 4e1f02db913d91f2988f559048e513e6093a1bce
     legacyGson = new GsonBuilder().setPrettyPrinting().create();
-    gson = ChangeNoteUtil.newGson();
+    gson = ChangeNoteJson.newGson();
   }
 
   @After
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 8a8c66e..f6d2568 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -152,6 +152,7 @@
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    initAfterLifecycleStart();
     setUpDatabase();
   }
 
@@ -171,6 +172,8 @@
     currentUserInfo = gApi.accounts().id(adminId.get()).get();
   }
 
+  protected void initAfterLifecycleStart() throws Exception {}
+
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
     return new RequestContext() {
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ba6c732..95f2df3 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -216,6 +216,7 @@
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    initAfterLifecycleStart();
     setUpDatabase();
   }
 
@@ -225,6 +226,8 @@
     db.close();
   }
 
+  protected void initAfterLifecycleStart() throws Exception {}
+
   protected void setUpDatabase() throws Exception {
     try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
       schemaCreator.create(underlyingDb);
@@ -2898,6 +2901,26 @@
     assertQuery("query:query4");
   }
 
+  @Test
+  public void byOwnerInvalidQuery() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    insert(repo, newChange(repo), userId);
+    String nameEmail = user.asIdentifiedUser().getNameEmail();
+    assertQuery("owner: \"" + nameEmail + "\"\\");
+  }
+
+  @Test
+  public void byDeletedChange() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(repo, newChange(repo));
+
+    String query = "change:" + change.getId();
+    assertQuery(query, change);
+
+    gApi.changes().id(change.getChangeId()).delete();
+    assertQuery(query);
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
     return newChange(repo, null, null, null, null, false);
   }
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 78ec176..09e3243 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -28,13 +28,12 @@
     ],
 )
 
+LUCENE_QUERY_TEST = ["LuceneQueryChangesTest.java"]
+
 junit_tests(
     name = "lucene_query_test",
     size = "large",
-    srcs = glob(
-        ["*.java"],
-        exclude = ABSTRACT_QUERY_TEST,
-    ),
+    srcs = LUCENE_QUERY_TEST,
     visibility = ["//visibility:public"],
     deps = [
         ":abstract_query_tests",
@@ -50,3 +49,26 @@
         "//lib/truth",
     ],
 )
+
+junit_tests(
+    name = "small_tests",
+    size = "small",
+    srcs = glob(
+        ["*.java"],
+        exclude = ABSTRACT_QUERY_TEST + LUCENE_QUERY_TEST,
+    ),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/cache/testing",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib:gwtorm",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
new file mode 100644
index 0000000..b87bbf7
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
@@ -0,0 +1,98 @@
+// 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.query.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
+import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ConflictKeyTest {
+  @Test
+  public void ffOnlyPreservesInputOrder() {
+    ObjectId id1 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
+    ObjectId id2 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    ConflictKey id1First = ConflictKey.create(id1, id2, FAST_FORWARD_ONLY, true);
+    ConflictKey id2First = ConflictKey.create(id2, id1, FAST_FORWARD_ONLY, true);
+
+    assertThat(id1First)
+        .isEqualTo(ConflictKey.createWithoutNormalization(id1, id2, FAST_FORWARD_ONLY, true));
+    assertThat(id2First)
+        .isEqualTo(ConflictKey.createWithoutNormalization(id2, id1, FAST_FORWARD_ONLY, true));
+    assertThat(id1First).isNotEqualTo(id2First);
+  }
+
+  @Test
+  public void nonFfOnlyNormalizesInputOrder() {
+    ObjectId id1 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
+    ObjectId id2 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    ConflictKey id1First = ConflictKey.create(id1, id2, MERGE_IF_NECESSARY, true);
+    ConflictKey id2First = ConflictKey.create(id2, id1, MERGE_IF_NECESSARY, true);
+    ConflictKey expected =
+        ConflictKey.createWithoutNormalization(id1, id2, MERGE_IF_NECESSARY, true);
+
+    assertThat(id1First).isEqualTo(expected);
+    assertThat(id2First).isEqualTo(expected);
+  }
+
+  @Test
+  public void serializer() throws Exception {
+    ConflictKey key =
+        ConflictKey.create(
+            ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"),
+            ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+            SubmitType.MERGE_IF_NECESSARY,
+            false);
+    byte[] serialized = ConflictKey.Serializer.INSTANCE.serialize(key);
+    assertThat(ConflictKeyProto.parseFrom(serialized))
+        .isEqualTo(
+            ConflictKeyProto.newBuilder()
+                .setCommit(
+                    bytes(
+                        0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee,
+                        0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee))
+                .setOtherCommit(
+                    bytes(
+                        0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
+                        0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
+                .setSubmitType("MERGE_IF_NECESSARY")
+                .setContentMerge(false)
+                .build());
+    assertThat(ConflictKey.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(key);
+  }
+
+  /**
+   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
+   * what to do if this test fails.
+   */
+  @Test
+  public void methods() throws Exception {
+    assertThatSerializedClass(ConflictKey.class)
+        .hasAutoValueMethods(
+            ImmutableMap.of(
+                "commit", ObjectId.class,
+                "otherCommit", ObjectId.class,
+                "submitType", SubmitType.class,
+                "contentMerge", boolean.class));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 5ee3aa4..1dfe7df 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -70,6 +70,7 @@
   }
 
   @Test
+  @Override
   public void byOwnerInvalidQuery() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo), userId);
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 686dd145..bacbb60 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -119,6 +119,7 @@
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    initAfterLifecycleStart();
     setUpDatabase();
   }
 
@@ -139,6 +140,8 @@
     currentUserInfo = gApi.accounts().id(userId.get()).get();
   }
 
+  protected void initAfterLifecycleStart() throws Exception {}
+
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
     return new RequestContext() {
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 8727afd..e34746c 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -102,6 +102,7 @@
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    initAfterLifecycleStart();
     setUpDatabase();
   }
 
@@ -121,6 +122,8 @@
     currentUserInfo = gApi.accounts().id(userId.get()).get();
   }
 
+  protected void initAfterLifecycleStart() throws Exception {}
+
   protected RequestContext newRequestContext(Account.Id requestUserId) {
     final CurrentUser requestUser = userFactory.create(requestUserId);
     return new RequestContext() {
diff --git a/lib/LICENSE-elasticsearch b/lib/LICENSE-elasticsearch
new file mode 100644
index 0000000..23cae9e
--- /dev/null
+++ b/lib/LICENSE-elasticsearch
@@ -0,0 +1,5 @@
+Elasticsearch
+Copyright 2009-2015 Elasticsearch
+
+This product includes software developed by The Apache Software
+Foundation (http://www.apache.org/).
diff --git a/lib/LICENSE-testcontainers b/lib/LICENSE-testcontainers
new file mode 100644
index 0000000..5d60e93
--- /dev/null
+++ b/lib/LICENSE-testcontainers
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Richard North
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/lib/asciidoctor/BUILD b/lib/asciidoctor/BUILD
index da05dd1..62b1114 100644
--- a/lib/asciidoctor/BUILD
+++ b/lib/asciidoctor/BUILD
@@ -1,48 +1,7 @@
-java_binary(
-    name = "asciidoc",
-    main_class = "AsciiDoctor",
-    visibility = ["//visibility:public"],
-    runtime_deps = [":asciidoc_lib"],
-)
-
-java_library(
-    name = "asciidoc_lib",
-    srcs = ["java/AsciiDoctor.java"],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":asciidoctor",
-        "//lib:args4j",
-        "//lib:guava",
-        "//lib/log:api",
-        "//lib/log:nop",
-    ],
-)
-
-java_binary(
-    name = "doc_indexer",
-    main_class = "DocIndexer",
-    visibility = ["//visibility:public"],
-    runtime_deps = [":doc_indexer_lib"],
-)
-
-java_library(
-    name = "doc_indexer_lib",
-    srcs = ["java/DocIndexer.java"],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":asciidoc_lib",
-        "//java/com/google/gerrit/server:constants",
-        "//lib:args4j",
-        "//lib:guava",
-        "//lib/lucene:lucene-analyzers-common",
-        "//lib/lucene:lucene-core-and-backward-codecs",
-    ],
-)
-
 java_library(
     name = "asciidoctor",
     data = ["//lib:LICENSE-asciidoctor"],
-    visibility = ["//visibility:public"],
+    visibility = ["//java/com/google/gerrit/asciidoctor:__pkg__"],
     exports = ["@asciidoctor//jar"],
     runtime_deps = [":jruby"],
 )
diff --git a/lib/elasticsearch-rest-client/BUILD b/lib/elasticsearch-rest-client/BUILD
new file mode 100644
index 0000000..c6357d0
--- /dev/null
+++ b/lib/elasticsearch-rest-client/BUILD
@@ -0,0 +1,8 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "elasticsearch-rest-client",
+    data = ["//lib:LICENSE-elasticsearch"],
+    visibility = ["//visibility:public"],
+    exports = ["@elasticsearch-rest-client//jar"],
+)
diff --git a/lib/elasticsearch/BUILD b/lib/elasticsearch/BUILD
deleted file mode 100644
index 13c033e..0000000
--- a/lib/elasticsearch/BUILD
+++ /dev/null
@@ -1,72 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-java_library(
-    name = "elasticsearch",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@elasticsearch//jar"],
-    runtime_deps = [
-        ":compress-lzf",
-        ":hppc",
-        ":joda-time",
-        ":jsr166e",
-        ":netty",
-        ":t-digest",
-        "//lib/jackson:jackson-core",
-        "//lib/jackson:jackson-dataformat-cbor",
-        "//lib/jackson:jackson-dataformat-smile",
-        "//lib/lucene:lucene-highlighter",
-        "//lib/lucene:lucene-join",
-        "//lib/lucene:lucene-memory",
-        "//lib/lucene:lucene-queries",
-        "//lib/lucene:lucene-spatial",
-        "//lib/lucene:lucene-suggest",
-    ],
-)
-
-java_library(
-    name = "joda-time",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@joda_time//jar"],
-    runtime_deps = ["joda-convert"],
-)
-
-java_library(
-    name = "joda-convert",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@joda_convert//jar"],
-)
-
-java_library(
-    name = "compress-lzf",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//lib/elasticsearch:__pkg__"],
-    exports = ["@compress_lzf//jar"],
-)
-
-java_library(
-    name = "hppc",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//lib/elasticsearch:__pkg__"],
-    exports = ["@hppc//jar"],
-)
-
-java_library(
-    name = "jsr166e",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//lib/elasticsearch:__pkg__"],
-    exports = ["@jsr166e//jar"],
-)
-
-java_library(
-    name = "netty",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//lib/elasticsearch:__pkg__"],
-    exports = ["@netty//jar"],
-)
-
-java_library(
-    name = "t-digest",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//lib/elasticsearch:__pkg__"],
-    exports = ["@t_digest//jar"],
-)
diff --git a/lib/flogger/BUILD b/lib/flogger/BUILD
new file mode 100644
index 0000000..c41e12f
--- /dev/null
+++ b/lib/flogger/BUILD
@@ -0,0 +1,10 @@
+java_library(
+    name = "api",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = [
+        "@flogger-log4j-backend//jar",
+        "@flogger-system-backend//jar",
+        "@flogger//jar",
+    ],
+)
diff --git a/lib/guava.bzl b/lib/guava.bzl
index e90c2b3..069149b 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "25.0-jre"
+GUAVA_VERSION = "25.1-jre"
 
-GUAVA_BIN_SHA1 = "7319c34fa5866a85b6bad445adad69d402323129"
+GUAVA_BIN_SHA1 = "6c57e4b22b44e89e548b5c9f70f0c45fe10fb0b4"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/highlightjs/building.md b/lib/highlightjs/building.md
index b35592f..bd1cd54 100644
--- a/lib/highlightjs/building.md
+++ b/lib/highlightjs/building.md
@@ -39,7 +39,6 @@
           kotlin \
           lisp \
           lua \
-          markdown \
           objectivec \
           ocaml \
           perl \
diff --git a/lib/highlightjs/highlight.min.js b/lib/highlightjs/highlight.min.js
index 069a018..9775c0d 100644
--- a/lib/highlightjs/highlight.min.js
+++ b/lib/highlightjs/highlight.min.js
@@ -1,124 +1,62 @@
 /*
  highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
-var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,g,l){b!=Array.prototype&&b!=Object.prototype&&(b[g]=l.value)};$jscomp.getGlobal=function(b){return"undefined"!=typeof window&&window===b?b:"undefined"!=typeof global&&null!=global?global:b};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_";
-$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(b){return $jscomp.SYMBOL_PREFIX+(b||"")+$jscomp.symbolCounter_++};
-$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var b=$jscomp.global.Symbol.iterator;b||(b=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[b]&&$jscomp.defineProperty(Array.prototype,b,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(b){var g=0;return $jscomp.iteratorPrototype(function(){return g<b.length?{done:!1,value:b[g++]}:{done:!0}})};
-$jscomp.iteratorPrototype=function(b){$jscomp.initSymbolIterator();b={next:b};b[$jscomp.global.Symbol.iterator]=function(){return this};return b};$jscomp.iteratorFromArray=function(b,g){$jscomp.initSymbolIterator();b instanceof String&&(b+="");var l=0,k={next:function(){if(l<b.length){var m=l++;return{value:g(m,b[m]),done:!1}}k.next=function(){return{done:!0,value:void 0}};return k.next()}};k[Symbol.iterator]=function(){return k};return k};
-$jscomp.polyfill=function(b,g,l,k){if(g){l=$jscomp.global;b=b.split(".");for(k=0;k<b.length-1;k++){var m=b[k];m in l||(l[m]={});l=l[m]}b=b[b.length-1];k=l[b];g=g(k);g!=k&&null!=g&&$jscomp.defineProperty(l,b,{configurable:!0,writable:!0,value:g})}};$jscomp.polyfill("Array.prototype.keys",function(b){return b?b:function(){return $jscomp.iteratorFromArray(this,function(b){return b})}},"es6","es3");
-(function(b){var g="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):g&&(g.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return g.hljs}))})(function(b){function g(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function l(a,f){return(a=a&&a.exec(f))&&0===a.index}function k(a){var f,b={},e=Array.prototype.slice.call(arguments,1);for(f in a)b[f]=a[f];e.forEach(function(a){for(f in a)b[f]=a[f]});
-return b}function m(a){var f=[];(function e(a,b){for(a=a.firstChild;a;a=a.nextSibling)3===a.nodeType?b+=a.nodeValue.length:1===a.nodeType&&(f.push({event:"start",offset:b,node:a}),b=e(a,b),a.nodeName.toLowerCase().match(/br|hr|img|input/)||f.push({event:"stop",offset:b,node:a}));return b})(a,0);return f}function L(a,f,b){function d(){return a.length&&f.length?a[0].offset!==f[0].offset?a[0].offset<f[0].offset?a:f:"start"===f[0].event?a:f:a.length?a:f}function c(a){B+="<"+a.nodeName.toLowerCase()+H.map.call(a.attributes,
-function(a){return" "+a.nodeName+'="'+g(a.value).replace('"',"&quot;")+'"'}).join("")+">"}function q(a){B+="</"+a.nodeName.toLowerCase()+">"}function w(a){("start"===a.event?c:q)(a.node)}for(var r=0,B="",n=[];a.length||f.length;){var h=d();B+=g(b.substring(r,h[0].offset));r=h[0].offset;if(h===a){n.reverse().forEach(q);do w(h.splice(0,1)[0]),h=d();while(h===a&&h.length&&h[0].offset===r);n.reverse().forEach(c)}else"start"===h[0].event?n.push(h[0].node):n.pop(),w(h.splice(0,1)[0])}return B+g(b.substr(r))}
-function M(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(f){return k(a,{variants:null},f)}));return a.cached_variants||a.endsWithParent&&[k(a)]||[a]}function N(a){function f(a){return a&&a.source||a}function b(b,d){return new RegExp(f(b),"m"+(a.case_insensitive?"i":"")+(d?"g":""))}function e(c,d){if(!c.compiled){c.compiled=!0;c.keywords=c.keywords||c.beginKeywords;if(c.keywords){var q={},g=function(f,b){a.case_insensitive&&(b=b.toLowerCase());b.split(" ").forEach(function(a){a=
-a.split("|");q[a[0]]=[f,a[1]?Number(a[1]):1]})};"string"===typeof c.keywords?g("keyword",c.keywords):x(c.keywords).forEach(function(a){g(a,c.keywords[a])});c.keywords=q}c.lexemesRe=b(c.lexemes||/\w+/,!0);d&&(c.beginKeywords&&(c.begin="\\b("+c.beginKeywords.split(" ").join("|")+")\\b"),c.begin||(c.begin=/\B|\b/),c.beginRe=b(c.begin),c.end||c.endsWithParent||(c.end=/\B|\b/),c.end&&(c.endRe=b(c.end)),c.terminator_end=f(c.end)||"",c.endsWithParent&&d.terminator_end&&(c.terminator_end+=(c.end?"|":"")+
-d.terminator_end));c.illegal&&(c.illegalRe=b(c.illegal));null==c.relevance&&(c.relevance=1);c.contains||(c.contains=[]);c.contains=Array.prototype.concat.apply([],c.contains.map(function(a){return M("self"===a?c:a)}));c.contains.forEach(function(a){e(a,c)});c.starts&&e(c.starts,d);d=c.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([c.terminator_end,c.illegal]).map(f).filter(Boolean);c.terminators=d.length?b(d.join("|"),!0):{exec:function(){return null}}}}
-e(a)}function C(a,f,b,e){function c(a,b){if(l(a.endRe,b)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return c(a.parent,b)}function d(a,b,f,d){return'<span class="'+(d?"":t.classPrefix)+(a+'">')+b+(f?"":"</span>")}function w(){var a=v,b;if(null!=h.subLanguage)if((b="string"===typeof h.subLanguage)&&!y[h.subLanguage])b=g(p);else{var f=b?C(h.subLanguage,p,!0,m[h.subLanguage]):F(p,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(u+=f.relevance);b&&(m[h.subLanguage]=
-f.top);b=d(f.language,f.value,!1,!0)}else if(h.keywords){f="";var c=0;h.lexemesRe.lastIndex=0;for(b=h.lexemesRe.exec(p);b;){f+=g(p.substring(c,b.index));c=h;var e=b;e=n.case_insensitive?e[0].toLowerCase():e[0];(c=c.keywords.hasOwnProperty(e)&&c.keywords[e])?(u+=c[1],f+=d(c[0],g(b[0]))):f+=g(b[0]);c=h.lexemesRe.lastIndex;b=h.lexemesRe.exec(p)}b=f+g(p.substr(c))}else b=g(p);v=a+b;p=""}function r(a){v+=a.className?d(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function k(a,f){p+=a;if(null==
-f)return w(),0;a:{a=h;var d;var e=0;for(d=a.contains.length;e<d;e++)if(l(a.contains[e].beginRe,f)){a=a.contains[e];break a}a=void 0}if(a)return a.skip?p+=f:(a.excludeBegin&&(p+=f),w(),a.returnBegin||a.excludeBegin||(p=f)),r(a,f),a.returnBegin?0:f.length;if(a=c(h,f)){e=h;e.skip?p+=f:(e.returnEnd||e.excludeEnd||(p+=f),w(),e.excludeEnd&&(p=f));do h.className&&(v+="</span>"),h.skip||(u+=h.relevance),h=h.parent;while(h!==a.parent);a.starts&&r(a.starts,"");return e.returnEnd?0:f.length}if(!b&&l(h.illegalRe,
-f))throw Error('Illegal lexeme "'+f+'" for mode "'+(h.className||"<unnamed>")+'"');p+=f;return f.length||1}var n=z(a);if(!n)throw Error('Unknown language: "'+a+'"');N(n);var h=e||n,m={},v="";for(e=h;e!==n;e=e.parent)e.className&&(v=d(e.className,"",!0)+v);var p="",u=0;try{for(var A,x,D=0;;){h.terminators.lastIndex=D;A=h.terminators.exec(f);if(!A)break;x=k(f.substring(D,A.index),A[0]);D=A.index+x}k(f.substr(D));for(e=h;e.parent;e=e.parent)e.className&&(v+="</span>");return{relevance:u,value:v,language:a,
-top:h}}catch(E){if(E.message&&-1!==E.message.indexOf("Illegal"))return{relevance:0,value:g(f)};throw E;}}function F(a,f){f=f||t.languages||x(y);var b={relevance:0,value:g(a)},e=b;f.filter(z).forEach(function(f){var c=C(f,a,!1);c.language=f;c.relevance>e.relevance&&(e=c);c.relevance>b.relevance&&(e=b,b=c)});e.language&&(b.second_best=e);return b}function I(a){return t.tabReplace||t.useBR?a.replace(O,function(a,b){return t.useBR&&"\n"===a?"<br>":t.tabReplace?b.replace(/\t/g,t.tabReplace):""}):a}function J(a){var b,
-d;a:{var e=a.className+" ";e+=a.parentNode?a.parentNode.className:"";if(d=P.exec(e))d=z(d[1])?d[1]:"no-highlight";else{e=e.split(/\s+/);d=0;for(b=e.length;d<b;d++){var c=e[d];if(K.test(c)||z(c)){d=c;break a}}d=void 0}}if(!K.test(d)){t.useBR?(c=document.createElementNS("http://www.w3.org/1999/xhtml","div"),c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):c=a;b=c.textContent;e=d?C(d,b,!0):F(b);c=m(c);if(c.length){var q=document.createElementNS("http://www.w3.org/1999/xhtml","div");
-q.innerHTML=e.value;e.value=L(c,m(q),b)}e.value=I(e.value);a.innerHTML=e.value;b=a.className;d=d?G[d]:e.language;c=[b.trim()];b.match(/\bhljs\b/)||c.push("hljs");-1===b.indexOf(d)&&c.push(d);d=c.join(" ").trim();a.className=d;a.result={language:e.language,re:e.relevance};e.second_best&&(a.second_best={language:e.second_best.language,re:e.second_best.relevance})}}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");H.forEach.call(a,J)}}function z(a){a=(a||"").toLowerCase();
-return y[a]||y[G[a]]}var H=[],x=Object.keys,y={},G={},K=/^(no-?highlight|plain|text)$/i,P=/\blang(?:uage)?-([\w-]+)\b/i,O=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,t={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};b.highlight=C;b.highlightAuto=F;b.fixMarkup=I;b.highlightBlock=J;b.configure=function(a){t=k(t,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,f){f=y[a]=f(b);f.aliases&&
-f.aliases.forEach(function(b){G[b]=a})};b.listLanguages=function(){return x(y)};b.getLanguage=z;b.inherit=k;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE=
-{begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/};b.COMMENT=function(a,f,d){a=b.inherit({className:"comment",begin:a,end:f,contains:[]},d||{});
+(function(b){var l="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):l&&(l.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return l.hljs}))})(function(b){function l(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function C(a,d){var e=a&&a.exec(d);return e&&0===e.index}function r(a){var d,e={},b=Array.prototype.slice.call(arguments,1);for(d in a)e[d]=a[d];b.forEach(function(a){for(d in a)e[d]=
+a[d]});return e}function G(a){var d=[];(function h(a,b){for(var k=a.firstChild;k;k=k.nextSibling)3===k.nodeType?b+=k.nodeValue.length:1===k.nodeType&&(d.push({event:"start",offset:b,node:k}),b=h(k,b),k.nodeName.toLowerCase().match(/br|hr|img|input/)||d.push({event:"stop",offset:b,node:k}));return b})(a,0);return d}function L(a,d,e){function b(){return a.length&&d.length?a[0].offset!==d[0].offset?a[0].offset<d[0].offset?a:d:"start"===d[0].event?a:d:a.length?a:d}function c(a){n+="<"+a.nodeName.toLowerCase()+
+H.map.call(a.attributes,function(a){return" "+a.nodeName+'="'+l(a.value).replace('"',"&quot;")+'"'}).join("")+">"}function f(a){n+="</"+a.nodeName.toLowerCase()+">"}function k(a){("start"===a.event?c:f)(a.node)}for(var v=0,n="",p=[];a.length||d.length;){var g=b(),n=n+l(e.substring(v,g[0].offset)),v=g[0].offset;if(g===a){p.reverse().forEach(f);do k(g.splice(0,1)[0]),g=b();while(g===a&&g.length&&g[0].offset===v);p.reverse().forEach(c)}else"start"===g[0].event?p.push(g[0].node):p.pop(),k(g.splice(0,
+1)[0])}return n+l(e.substr(v))}function M(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(d){return r(a,{variants:null},d)}));return a.cached_variants||a.endsWithParent&&[r(a)]||[a]}function N(a){function d(a){return a&&a.source||a}function e(c,e){return new RegExp(d(c),"m"+(a.case_insensitive?"i":"")+(e?"g":""))}function b(c,f){if(!c.compiled){c.compiled=!0;c.keywords=c.keywords||c.beginKeywords;if(c.keywords){var k={},l=function(d,c){a.case_insensitive&&(c=c.toLowerCase());
+c.split(" ").forEach(function(a){a=a.split("|");k[a[0]]=[d,a[1]?Number(a[1]):1]})};"string"===typeof c.keywords?l("keyword",c.keywords):w(c.keywords).forEach(function(a){l(a,c.keywords[a])});c.keywords=k}c.lexemesRe=e(c.lexemes||/\w+/,!0);f&&(c.beginKeywords&&(c.begin="\\b("+c.beginKeywords.split(" ").join("|")+")\\b"),c.begin||(c.begin=/\B|\b/),c.beginRe=e(c.begin),c.end||c.endsWithParent||(c.end=/\B|\b/),c.end&&(c.endRe=e(c.end)),c.terminator_end=d(c.end)||"",c.endsWithParent&&f.terminator_end&&
+(c.terminator_end+=(c.end?"|":"")+f.terminator_end));c.illegal&&(c.illegalRe=e(c.illegal));null==c.relevance&&(c.relevance=1);c.contains||(c.contains=[]);c.contains=Array.prototype.concat.apply([],c.contains.map(function(a){return M("self"===a?c:a)}));c.contains.forEach(function(a){b(a,c)});c.starts&&b(c.starts,f);var n=c.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([c.terminator_end,c.illegal]).map(d).filter(Boolean);c.terminators=n.length?e(n.join("|"),
+!0):{exec:function(){return null}}}}b(a)}function A(a,d,e,b){function c(a,d){if(C(a.endRe,d)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return c(a.parent,d)}function f(a,d,b,c){return'<span class="'+(c?"":q.classPrefix)+(a+'">')+d+(b?"":"</span>")}function k(){var a=t,d;if(null!=g.subLanguage)if((d="string"===typeof g.subLanguage)&&!x[g.subLanguage])d=l(m);else{var b=d?A(g.subLanguage,m,!0,u[g.subLanguage]):E(m,g.subLanguage.length?g.subLanguage:void 0);0<g.relevance&&(r+=
+b.relevance);d&&(u[g.subLanguage]=b.top);d=f(b.language,b.value,!1,!0)}else{var c;if(g.keywords){b="";c=0;g.lexemesRe.lastIndex=0;for(d=g.lexemesRe.exec(m);d;){b+=l(m.substring(c,d.index));c=g;var e=d,e=p.case_insensitive?e[0].toLowerCase():e[0];(c=c.keywords.hasOwnProperty(e)&&c.keywords[e])?(r+=c[1],b+=f(c[0],l(d[0]))):b+=l(d[0]);c=g.lexemesRe.lastIndex;d=g.lexemesRe.exec(m)}d=b+l(m.substr(c))}else d=l(m)}t=a+d;m=""}function v(a){t+=a.className?f(a.className,"",!0):"";g=Object.create(a,{parent:{value:g}})}
+function n(a,d){m+=a;if(null==d)return k(),0;var b;a:{b=g;var f,h;f=0;for(h=b.contains.length;f<h;f++)if(C(b.contains[f].beginRe,d)){b=b.contains[f];break a}b=void 0}if(b)return b.skip?m+=d:(b.excludeBegin&&(m+=d),k(),b.returnBegin||b.excludeBegin||(m=d)),v(b,d),b.returnBegin?0:d.length;if(b=c(g,d)){f=g;f.skip?m+=d:(f.returnEnd||f.excludeEnd||(m+=d),k(),f.excludeEnd&&(m=d));do g.className&&(t+="</span>"),g.skip||(r+=g.relevance),g=g.parent;while(g!==b.parent);b.starts&&v(b.starts,"");return f.returnEnd?
+0:d.length}if(!e&&C(g.illegalRe,d))throw Error('Illegal lexeme "'+d+'" for mode "'+(g.className||"<unnamed>")+'"');m+=d;return d.length||1}var p=y(a);if(!p)throw Error('Unknown language: "'+a+'"');N(p);var g=b||p,u={},t="";for(b=g;b!==p;b=b.parent)b.className&&(t=f(b.className,"",!0)+t);var m="",r=0;try{for(var z,w,B=0;;){g.terminators.lastIndex=B;z=g.terminators.exec(d);if(!z)break;w=n(d.substring(B,z.index),z[0]);B=z.index+w}n(d.substr(B));for(b=g;b.parent;b=b.parent)b.className&&(t+="</span>");
+return{relevance:r,value:t,language:a,top:g}}catch(D){if(D.message&&-1!==D.message.indexOf("Illegal"))return{relevance:0,value:l(d)};throw D;}}function E(a,d){d=d||q.languages||w(x);var b={relevance:0,value:l(a)},h=b;d.filter(y).forEach(function(d){var f=A(d,a,!1);f.language=d;f.relevance>h.relevance&&(h=f);f.relevance>b.relevance&&(h=b,b=f)});h.language&&(b.second_best=h);return b}function I(a){return q.tabReplace||q.useBR?a.replace(O,function(a,b){return q.useBR&&"\n"===a?"<br>":q.tabReplace?b.replace(/\t/g,
+q.tabReplace):""}):a}function J(a){var d,b,h,c,f;a:if(b=a.className+" ",b+=a.parentNode?a.parentNode.className:"",f=P.exec(b))f=y(f[1])?f[1]:"no-highlight";else{b=b.split(/\s+/);f=0;for(c=b.length;f<c;f++)if(d=b[f],K.test(d)||y(d)){f=d;break a}f=void 0}K.test(f)||(q.useBR?(d=document.createElementNS("http://www.w3.org/1999/xhtml","div"),d.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):d=a,c=d.textContent,b=f?A(f,c,!0):E(c),d=G(d),d.length&&(h=document.createElementNS("http://www.w3.org/1999/xhtml",
+"div"),h.innerHTML=b.value,b.value=L(d,G(h),c)),b.value=I(b.value),a.innerHTML=b.value,c=a.className,f=f?F[f]:b.language,d=[c.trim()],c.match(/\bhljs\b/)||d.push("hljs"),-1===c.indexOf(f)&&d.push(f),f=d.join(" ").trim(),a.className=f,a.result={language:b.language,re:b.relevance},b.second_best&&(a.second_best={language:b.second_best.language,re:b.second_best.relevance}))}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");H.forEach.call(a,J)}}function y(a){a=(a||"").toLowerCase();
+return x[a]||x[F[a]]}var H=[],w=Object.keys,x={},F={},K=/^(no-?highlight|plain|text)$/i,P=/\blang(?:uage)?-([\w-]+)\b/i,O=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,q={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};b.highlight=A;b.highlightAuto=E;b.fixMarkup=I;b.highlightBlock=J;b.configure=function(a){q=r(q,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,d){var e=x[a]=d(b);e.aliases&&
+e.aliases.forEach(function(b){F[b]=a})};b.listLanguages=function(){return w(x)};b.getLanguage=y;b.inherit=r;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE=
+{begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/};b.COMMENT=function(a,d,e){a=b.inherit({className:"comment",begin:a,end:d,contains:[]},e||{});
 a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#","$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",
 begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};
-b.registerLanguage("bash",function(a){var b={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},d={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b,{className:"variable",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/\b-?[a-z\._]+\b/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",
-_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,d,{className:"string",begin:/'/,end:/'/},b]}});b.registerLanguage("clojure",function(a){var b={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},d=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),e=a.COMMENT(";","$",{relevance:0}),
-c={className:"literal",begin:/\b(true|false|nil)\b/},q={begin:"[\\[\\{]",end:"[\\]\\}]"},g={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},r=a.COMMENT("\\^\\{","\\}"),k={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},n={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},l={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},
-lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},m=[n,d,g,r,e,k,q,b,c,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];n.contains=[a.COMMENT("comment",""),l,h];h.contains=m;q.contains=m;r.contains=[q];return{aliases:["clj"],illegal:/\S/,contains:[n,d,g,r,e,k,q,b,c]}});b.registerLanguage("cpp",function(a){var b={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},d={className:"string",
-variants:[{begin:'(u8?|U)?L?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},e={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},
-contains:[{begin:/\\\n/,relevance:0},a.inherit(d,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},q=a.IDENT_RE+"\\s*\\(",g={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",
+b.registerLanguage("bash",function(a){var b={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},e={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b,{className:"variable",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/\b-?[a-z\._]+\b/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",
+_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,e,{className:"string",begin:/'/,end:/'/},b]}});b.registerLanguage("cpp",function(a){var b={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},e={className:"string",variants:[{begin:'(u8?|U)?L?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},
+{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},h={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},contains:[{begin:/\\\n/,
+relevance:0},a.inherit(e,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f=a.IDENT_RE+"\\s*\\(",k={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",
 built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",
-literal:"true false nullptr NULL"},k=[b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,d];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:g,illegal:"</",contains:k.concat([c,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:g,contains:["self",b]},{begin:a.IDENT_RE+"::",keywords:g},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
-end:/;/}],keywords:g,contains:k.concat([{begin:/\(/,end:/\)/,keywords:g,contains:k.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+q,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:g,illegal:/[^\w\s\*&]/,contains:[{begin:q,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,e,b]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,
-c]},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:c,strings:d,keywords:g}}});b.registerLanguage("cs",function(a){var b={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",
-literal:"null false true"},d={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},e=a.inherit(d,{illegal:/\n/}),c={className:"subst",begin:"{",end:"}",keywords:b},g=a.inherit(c,{illegal:/\n/}),k={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,g]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},c]},m=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},g]});c.contains=
-[l,k,d,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE];g.contains=[m,k,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];d={variants:[l,k,d,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};e=a.IDENT_RE+"(<"+a.IDENT_RE+"(\\s*,\\s*"+a.IDENT_RE+")*>)?(\\[\\])?";return{aliases:["csharp"],keywords:b,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},
-{begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a.C_NUMBER_MODE,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),
-a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+e+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:b,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,
-keywords:b,relevance:0,contains:[d,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(font-face|page)",
-lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",
-excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var b=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE,
-keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",
-built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},{className:"string",begin:'"',contains:[{begin:"\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",relevance:0}],
-end:'"[cwd]?'},{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},{className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(i|[fF]i|Li))",
-relevance:0},{className:"number",begin:"\\b((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(L|u|U|Lu|LU|uL|UL)?",relevance:0},{className:"string",begin:"'(\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};|.)",end:"'",illegal:"."},{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}});b.registerLanguage("markdown",
-function(a){return{aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$"},{begin:"^.+?\\n[=-]{2,}$"}]},{begin:"<",end:">",subLanguage:"xml",relevance:0},{className:"bullet",begin:"^([*+-]|(\\d+\\.))\\s+"},{className:"strong",begin:"[*_]{2}.+?[*_]{2}"},{className:"emphasis",variants:[{begin:"\\*.+?\\*"},{begin:"_.+?_",relevance:0}]},{className:"quote",begin:"^>\\s+",end:"$"},{className:"code",variants:[{begin:"^```w*s*$",end:"^```s*$"},{begin:"`.+?`"},{begin:"^( {4}|\t)",
-end:"$",relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},{begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",
-begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var b={className:"subst",begin:"\\$\\{",end:"}",keywords:"true false null this is new super"},d={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,
-b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]}]};b.contains=[a.C_NUMBER_MODE,d];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"},
-contains:[d,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",
-relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",
-end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>",
-contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}});b.registerLanguage("ruby",function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},d={className:"doctag",begin:"@[A-Za-z]+"},e={begin:"#<",end:">"};d=[a.COMMENT("#","$",{contains:[d]}),a.COMMENT("^\\=begin",
-"^\\=end",{contains:[d],relevance:10}),a.COMMENT("^__END__","\\n$")];var c={className:"subst",begin:"#\\{",end:"}",keywords:b},g={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},
-{begin:/<<(-?)\w+$/,end:/^\s*\w+$/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[g,e,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(d)},{className:"function",beginKeywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),
-k].concat(d)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+
-"|unless)\\s*",keywords:"unless",contains:[e,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(d),relevance:0}].concat(d);c.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],keywords:b,illegal:/\/\*/,contains:d.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",
-starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("erb",function(a){return{subLanguage:"xml",contains:[a.COMMENT("<%#","%>"),{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",
+literal:"true false nullptr NULL"},l=[b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,h,e];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:k,illegal:"</",contains:l.concat([c,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:k,contains:["self",b]},{begin:a.IDENT_RE+"::",keywords:k},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",
+end:/;/}],keywords:k,contains:l.concat([{begin:/\(/,end:/\)/,keywords:k,contains:l.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+f,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:k,illegal:/[^\w\s\*&]/,contains:[{begin:f,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:k,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,h,b]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,
+c]},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:c,strings:e,keywords:k}}});b.registerLanguage("css",function(a){return{case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},
+{begin:"@(font-face|page)",lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",
+begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",
 literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],keywords:b,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[dflsi]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:/\s*\{/,excludeEnd:!0,
-contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},d={className:"meta",begin:"{-#",end:"#-}"},e={className:"meta",begin:"^#",end:"$"},c={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},g={begin:"\\(",end:"\\)",illegal:'"',contains:[d,e,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}),
-b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[g,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[g,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",
-end:"where",keywords:"class family instance where",contains:[c,g,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[d,c,g,{begin:"{",end:"}",contains:g.contains},b]},{beginKeywords:"default",end:"$",contains:[c,g,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[c,a.QUOTE_STRING_MODE,
-b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},d,e,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,c,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",
+contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},e={className:"meta",begin:"{-#",end:"#-}"},h={className:"meta",begin:"^#",end:"$"},c={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},f={begin:"\\(",end:"\\)",illegal:'"',contains:[e,h,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}),
+b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[f,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[f,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",
+end:"where",keywords:"class family instance where",contains:[c,f,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[e,c,f,{begin:"{",end:"}",contains:f.contains},b]},{beginKeywords:"default",end:"$",contains:[c,f,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[c,a.QUOTE_STRING_MODE,
+b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},e,h,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,c,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",
 illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(<[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+
 a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
 contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,
 a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){var b={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",
 literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},
-d={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"subst",begin:"\\$\\{",end:"\\}",keywords:b,contains:[]},c={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,e]};e.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,d,a.REGEXP_MODE];e=e.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:b,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},
-{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z$_][0-9A-Za-z$_]*)\\s*=>",
-returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:e}]}]},{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),
-{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:e}],illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},d=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],e={end:",",endsWithParent:!0,excludeEnd:!0,
-contains:d,keywords:b},c={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(e,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(e)],illegal:"\\S"};d.splice(d.length,0,c,a);return{contains:d,keywords:b,illegal:"\\S"}});b.registerLanguage("kotlin",function(a){var b={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit initinterface annotation data sealed internal infix operator out by constructor super trait volatile transient native default",
-built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},d={className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"@"},e={className:"subst",begin:"\\${",end:"}",contains:[a.APOS_STRING_MODE,a.C_NUMBER_MODE]},c={className:"variable",begin:"\\$"+a.UNDERSCORE_IDENT_RE};e={className:"string",variants:[{begin:'"""',end:'"""',contains:[c,e]},{begin:"'",end:"'",illegal:/\n/,contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[a.BACKSLASH_ESCAPE,c,
-e]}]};c={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+a.UNDERSCORE_IDENT_RE+")?"};var g={className:"meta",begin:"@"+a.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[a.inherit(e,{className:"meta-string"})]}]};return{keywords:b,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"keyword",begin:/\b(break|continue|return|this)\b/,
-starts:{contains:[{className:"symbol",begin:/@\w+/}]}},d,c,g,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:b,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
-endsWithParent:!0,contains:[{className:"type",begin:a.UNDERSCORE_IDENT_RE},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],relevance:0},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,g,e,a.C_NUMBER_MODE]},a.C_BLOCK_COMMENT_MODE]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},a.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,excludeEnd:!0,
-relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},c,g]},e,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},a.C_NUMBER_MODE]}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},d={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},
-{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},e=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var c={begin:"\\*",end:"\\*"},g={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",relevance:0},
-l={contains:[d,e,c,g,{begin:"\\(",end:"\\)",contains:["self",b,e,d,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},m={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},n={begin:"\\(\\s*",end:"\\)"},h={endsWithParent:!0,
-relevance:0};n.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[l,m,n,b,d,e,a,c,g,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[d,{className:"meta",begin:"^#!",end:"$"},b,e,a,l,m,n,k]}});b.registerLanguage("lua",function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},d=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b],relevance:10})];
-return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},
-contains:d.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:d}].concat(d)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("objectivec",function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/;return{aliases:["mm","objc","obj-c"],keywords:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",
-literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},lexemes:b,illegal:"</",contains:[{className:"built_in",begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:"'",end:"[^\\\\]'",illegal:"[^\\\\][^']"}]},
-{className:"meta",begin:"#",end:"$",contains:[{className:"meta-string",variants:[{begin:'"',end:'"'},{begin:"<",end:">"}]}]},{className:"class",begin:"(@interface|@class|@protocol|@implementation)\\b",end:"({|$)",excludeEnd:!0,keywords:"@interface @class @protocol @implementation",lexemes:b,contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"\\."+a.UNDERSCORE_IDENT_RE,relevance:0}]}});b.registerLanguage("ocaml",function(a){return{aliases:["ml"],keywords:{keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value",
-built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",literal:"true false"},illegal:/\/\/|>>/,lexemes:"[a-z_]\\w*!?",contains:[{className:"literal",begin:"\\[(\\|\\|)?\\]|\\(\\)",relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*",relevance:0},
-a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",relevance:0},{begin:/[-=]>/}]}});b.registerLanguage("perl",function(a){var b={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},
-d={begin:"->{",end:"}"},e={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},c=[a.BACKSLASH_ESCAPE,b,e];a=[e,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),d,{className:"string",contains:c,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",
+e={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},h={className:"subst",begin:"\\$\\{",end:"\\}",keywords:b,contains:[]},c={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,h]};h.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,e,a.REGEXP_MODE];h=h.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:b,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},
+{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z$_][0-9A-Za-z$_]*)\\s*=>",
+returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:h}]}]},{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),
+{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:h}],illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},e=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],h={end:",",endsWithParent:!0,excludeEnd:!0,
+contains:e,keywords:b},c={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(h,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(h)],illegal:"\\S"};e.splice(e.length,0,c,a);return{contains:e,keywords:b,illegal:"\\S"}});b.registerLanguage("perl",function(a){var b={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},
+e={begin:"->{",end:"}"},h={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},c=[a.BACKSLASH_ESCAPE,b,h];a=[h,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),e,{className:"string",contains:c,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",
 end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+a.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",
 relevance:0,contains:[a.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[a.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[a.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];b.contains=
-a;d.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",
-contains:a}});b.registerLanguage("php",function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},d={className:"meta",begin:/<\?(php)?|\?>/},e={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},c={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["php3","php4","php5","php6"],case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",
-contains:[a.HASH_COMMENT_MODE,a.COMMENT("//","$",{contains:[d]}),a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:a.UNDERSCORE_IDENT_RE}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[a.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},d,{className:"keyword",begin:/\$this\b/},b,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},
-{className:"function",beginKeywords:"function",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",contains:["self",b,a.C_BLOCK_COMMENT_MODE,e,c]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]},
-{begin:"=>"},e,c]}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},
-{className:"function",beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("puppet",function(a){var b=a.COMMENT("#","$"),d=a.inherit(a.TITLE_MODE,{begin:"([A-Za-z_]|::)(\\w|::)*"}),e={className:"variable",begin:"\\$([A-Za-z_]|::)(\\w|::)*"},c={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}]};return{aliases:["pp"],contains:[b,e,c,{beginKeywords:"class",end:"\\{|;",
-illegal:/=/,contains:[d,b]},{beginKeywords:"define",end:/\{/,contains:[{className:"section",begin:a.IDENT_RE,endsParent:!0}]},{begin:a.IDENT_RE+"\\s+\\{",returnBegin:!0,end:/\S/,contains:[{className:"keyword",begin:a.IDENT_RE},{begin:/\{/,end:/\}/,keywords:{keyword:"and case default else elsif false if in import enherits node or true undef unless main settings $string ",literal:"alias audit before loglevel noop require subscribe tag owner ensure group mode name|0 changes context force incl lens load_path onlyif provider returns root show_diff type_check en_address ip_address realname command environment hour monute month monthday special target weekday creates cwd ogoutput refresh refreshonly tries try_sleep umask backup checksum content ctime force ignore links mtime purge recurse recurselimit replace selinux_ignore_defaults selrange selrole seltype seluser source souirce_permissions sourceselect validate_cmd validate_replacement allowdupe attribute_membership auth_membership forcelocal gid ia_load_module members system host_aliases ip allowed_trunk_vlans description device_url duplex encapsulation etherchannel native_vlan speed principals allow_root auth_class auth_type authenticate_user k_of_n mechanisms rule session_owner shared options device fstype enable hasrestart directory present absent link atboot blockdevice device dump pass remounts poller_tag use message withpath adminfile allow_virtual allowcdrom category configfiles flavor install_options instance package_settings platform responsefile status uninstall_options vendor unless_system_user unless_uid binary control flags hasstatus manifest pattern restart running start stop allowdupe auths expiry gid groups home iterations key_membership keys managehome membership password password_max_age password_min_age profile_membership profiles project purge_ssh_keys role_membership roles salt shell uid baseurl cost descr enabled enablegroups exclude failovermethod gpgcheck gpgkey http_caching include includepkgs keepalive metadata_expire metalink mirrorlist priority protect proxy proxy_password proxy_username repo_gpgcheck s3_enabled skip_if_unavailable sslcacert sslclientcert sslclientkey sslverify mounted",
-built_in:"architecture augeasversion blockdevices boardmanufacturer boardproductname boardserialnumber cfkey dhcp_servers domain ec2_ ec2_userdata facterversion filesystems ldom fqdn gid hardwareisa hardwaremodel hostname id|0 interfaces ipaddress ipaddress_ ipaddress6 ipaddress6_ iphostnumber is_virtual kernel kernelmajversion kernelrelease kernelversion kernelrelease kernelversion lsbdistcodename lsbdistdescription lsbdistid lsbdistrelease lsbmajdistrelease lsbminordistrelease lsbrelease macaddress macaddress_ macosx_buildversion macosx_productname macosx_productversion macosx_productverson_major macosx_productversion_minor manufacturer memoryfree memorysize netmask metmask_ network_ operatingsystem operatingsystemmajrelease operatingsystemrelease osfamily partitions path physicalprocessorcount processor processorcount productname ps puppetversion rubysitedir rubyversion selinux selinux_config_mode selinux_config_policy selinux_current_mode selinux_current_mode selinux_enforced selinux_policyversion serialnumber sp_ sshdsakey sshecdsakey sshrsakey swapencrypted swapfree swapsize timezone type uniqueid uptime uptime_days uptime_hours uptime_seconds uuid virtual vlans xendomains zfs_version zonenae zones zpool_version"},
-relevance:0,contains:[c,b,{begin:"[a-zA-Z_]+\\s*=>",returnBegin:!0,end:"=>",contains:[{className:"attr",begin:a.IDENT_RE}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},e]}],relevance:0}]}});b.registerLanguage("python",function(a){var b={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",
-built_in:"Ellipsis NotImplemented"},d={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"subst",begin:/\{/,end:/\}/,keywords:b,illegal:/#/},c={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[d],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[d],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[d,e]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[d,e]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},
-{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e]},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},k={className:"params",begin:/\(/,end:/\)/,contains:["self",d,g,c]};e.contains=[c,g,d];return{aliases:["py","gyp"],keywords:b,illegal:/(<\/|->|\?)|=>/,contains:[d,g,c,a.HASH_COMMENT_MODE,
-{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,k,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("rust",function(a){return{aliases:["rs"],keywords:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default",
-literal:"true false Some None Ok Err",built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"},
-lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)"(.|\n)*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0o([0-7_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},
-{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([ui](8|16|32|64|128|size)|f(32|64))?"}],relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct union",end:"{",
-contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+"::",keywords:{built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"}},
-{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},d={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},e={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},d,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[e]},{className:"class",beginKeywords:"class object trait type",
-end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},e]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("shell",function(a){return{aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}});b.registerLanguage("sql",
-function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",end:/;/,endsWithParent:!0,
-lexemes:/[\w\.]+/,keywords:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
+a;e.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",
+contains:a}});b.registerLanguage("prolog",function(a){var b={begin:/\(/,end:/\)/,relevance:0},e={begin:/\[/,end:/\]/};a=[{begin:/[a-z][A-Za-z0-9_]*/,relevance:0},{className:"symbol",variants:[{begin:/[A-Z][a-zA-Z0-9_]*/},{begin:/_[A-Za-z0-9_]*/}],relevance:0},b,{begin:/:-/},e,{className:"comment",begin:/%/,end:/$/,contains:[a.PHRASAL_WORDS_MODE]},a.C_BLOCK_COMMENT_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,{className:"string",begin:/`/,end:/`/,contains:[a.BACKSLASH_ESCAPE]},{className:"string",begin:/0\'(\\\'|.)/},
+{className:"string",begin:/0\'\\s/},a.C_NUMBER_MODE];b.contains=a;e.contains=a;return{contains:a.concat([{begin:/\.$/}])}});b.registerLanguage("python",function(a){var b={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},e={className:"meta",begin:/^(>>>|\.\.\.) /},h={className:"subst",begin:/\{/,end:/\}/,
+keywords:b,illegal:/#/},c={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e,h]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e,h]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[h]},{begin:/(fr|rf|f)"/,end:/"/,
+contains:[h]},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},f={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},k={className:"params",begin:/\(/,end:/\)/,contains:["self",e,f,c]};h.contains=[c,f,e];return{aliases:["py","gyp"],keywords:b,illegal:/(<\/|->|\?)|=>/,contains:[e,f,c,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,
+contains:[a.UNDERSCORE_TITLE_MODE,k,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},e={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},h={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",
+keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,
+b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},e,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[h]},{className:"class",beginKeywords:"class object trait type",end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,
+relevance:0,contains:[e]},h]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",
+end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",
 literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}]},{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,b]},
-a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",
-literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},
-d=a.COMMENT("/\\*","\\*/",{contains:["self"]}),e={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},c={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},g=a.inherit(a.QUOTE_STRING_MODE,{contains:[e,a.BACKSLASH_ESCAPE]});e.contains=[c];return{keywords:b,contains:[g,a.C_LINE_COMMENT_MODE,d,{className:"type",begin:"\\b[A-Z][\\w\u00c0-\u02b8']*",relevance:0},c,{className:"function",beginKeywords:"func",end:"{",
-excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",c,g,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},
-{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,d]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"};
-return{aliases:["ts"],keywords:b,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",
-contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+a.IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:a.IDENT_RE},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:["self",a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}]}],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),
-{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0,contains:["self",{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},
-{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},d={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[a.BACKSLASH_ESCAPE,{className:"template-variable",
-variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:d.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+
-a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},a.HASH_COMMENT_MODE,{beginKeywords:"true false yes no null",keywords:{literal:"true false yes no null"}},a.C_NUMBER_MODE,d]}});return b});
+a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",
+end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript",
+"javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}});return b});
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
index 8ade0cf..c01890d 100644
--- a/lib/jackson/BUILD
+++ b/lib/jackson/BUILD
@@ -7,15 +7,3 @@
     data = ["//lib:LICENSE-Apache2.0"],
     exports = ["@jackson_core//jar"],
 )
-
-java_library(
-    name = "jackson-dataformat-cbor",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@jackson_dataformat_cbor//jar"],
-)
-
-java_library(
-    name = "jackson-dataformat-smile",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@jackson_dataformat_smile//jar"],
-)
diff --git a/lib/jest/BUILD b/lib/jest/BUILD
deleted file mode 100644
index 169f271..0000000
--- a/lib/jest/BUILD
+++ /dev/null
@@ -1,23 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-java_library(
-    name = "jest-common",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@jest_common//jar"],
-    runtime_deps = [
-        "//lib/commons:lang3",
-    ],
-)
-
-java_library(
-    name = "jest",
-    data = ["//lib:LICENSE-Apache2.0"],
-    visibility = ["//visibility:public"],
-    exports = ["@jest//jar"],
-    runtime_deps = [
-        "//lib/httpcomponents:httpasyncclient",
-        "//lib/httpcomponents:httpclient",
-        "//lib/httpcomponents:httpcore-nio",
-    ],
-)
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
index 5586cb1..caf8eec 100644
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -5,7 +5,10 @@
     data = ["//lib:LICENSE-jgit"],
     visibility = ["//visibility:public"],
     exports = [jgit_dep("@jgit_lib//jar")],
-    runtime_deps = [":javaewah"],
+    runtime_deps = [
+        ":javaewah",
+        "//lib/log:api",
+    ],
 )
 
 alias(
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index 5ee3535..6b4e003 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -65,8 +65,8 @@
   bower_archive(
     name = "iron-menu-behavior",
     package = "PolymerElements/iron-menu-behavior",
-    version = "2.0.1",
-    sha1 = "139528ee1e8d86257e2aa445de7761b8ec70ae91")
+    version = "2.1.1",
+    sha1 = "1504997f6eb9aec490b855dadee473cac064f38c")
   bower_archive(
     name = "iron-meta",
     package = "PolymerElements/iron-meta",
@@ -105,8 +105,8 @@
   bower_archive(
     name = "paper-icon-button",
     package = "PolymerElements/paper-icon-button",
-    version = "2.1.0",
-    sha1 = "caead6a276877888d128ace809376980c3f3fe42")
+    version = "2.2.0",
+    sha1 = "9525e76ef433428bb9d6ec4fa65c4ef83156a803")
   bower_archive(
     name = "paper-ripple",
     package = "PolymerElements/paper-ripple",
diff --git a/lib/log/BUILD b/lib/log/BUILD
index af83d19..949260d 100644
--- a/lib/log/BUILD
+++ b/lib/log/BUILD
@@ -1,16 +1,18 @@
 java_library(
     name = "api",
     data = ["//lib:LICENSE-slf4j"],
-    visibility = ["//visibility:public"],
+    visibility = [
+        "//lib/jgit/org.eclipse.jgit:__pkg__",
+        "//plugins:__pkg__",
+    ],
     exports = ["@log_api//jar"],
 )
 
 java_library(
-    name = "nop",
+    name = "ext",
     data = ["//lib:LICENSE-slf4j"],
     visibility = ["//visibility:public"],
-    exports = ["@log_nop//jar"],
-    runtime_deps = [":api"],
+    exports = ["@log_ext//jar"],
 )
 
 java_library(
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index 6590af4..5c8982a 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -44,39 +44,3 @@
     exports = ["@lucene_queryparser//jar"],
     runtime_deps = [":lucene-core-and-backward-codecs"],
 )
-
-java_library(
-    name = "lucene-highlighter",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@lucene_highlighter//jar"],
-)
-
-java_library(
-    name = "lucene-join",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@lucene_join//jar"],
-)
-
-java_library(
-    name = "lucene-memory",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@lucene_memory//jar"],
-)
-
-java_library(
-    name = "lucene-spatial",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@lucene_spatial//jar"],
-)
-
-java_library(
-    name = "lucene-suggest",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@lucene_suggest//jar"],
-)
-
-java_library(
-    name = "lucene-queries",
-    data = ["//lib:LICENSE-Apache2.0"],
-    exports = ["@lucene_queries//jar"],
-)
diff --git a/lib/polymer_externs/BUILD b/lib/polymer_externs/BUILD
index 2f1bdbd..ae8f9c0 100644
--- a/lib/polymer_externs/BUILD
+++ b/lib/polymer_externs/BUILD
@@ -18,9 +18,16 @@
 
 load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
 
+genrule(
+    name = "polymer_closure_renamed",
+    srcs = ["@polymer_closure//file"],
+    outs = ["polymer_closure_renamed.js"],
+    cmd = "cp $< $@",
+)
+
 closure_js_library(
     name = "polymer_closure",
-    srcs = ["@polymer_closure//file"],
+    srcs = [":polymer_closure_renamed"],
     data = ["//lib:LICENSE-Apache2.0"],
     no_closure_library = True,
 )
diff --git a/lib/testcontainers/BUILD b/lib/testcontainers/BUILD
new file mode 100644
index 0000000..e6ec04f
--- /dev/null
+++ b/lib/testcontainers/BUILD
@@ -0,0 +1,37 @@
+java_library(
+    name = "duct-tape",
+    testonly = True,
+    data = ["//lib:LICENSE-testcontainers"],
+    visibility = ["//visibility:public"],
+    exports = ["@duct_tape//jar"],
+)
+
+java_library(
+    name = "visible-assertions",
+    testonly = True,
+    data = ["//lib:LICENSE-testcontainers"],
+    visibility = ["//visibility:public"],
+    exports = ["@visible_assertions//jar"],
+)
+
+java_library(
+    name = "jna",
+    testonly = True,
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@jna//jar"],
+)
+
+java_library(
+    name = "testcontainers",
+    testonly = True,
+    data = ["//lib:LICENSE-testcontainers"],
+    visibility = ["//visibility:public"],
+    exports = ["@testcontainers//jar"],
+    runtime_deps = [
+        ":duct-tape",
+        ":jna",
+        ":visible-assertions",
+        "//lib/log:ext",
+    ],
+)
diff --git a/plugins/BUILD b/plugins/BUILD
index 471ab3d..3852bc1 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -36,11 +36,12 @@
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/metrics/dropwizard",
     "//java/com/google/gerrit/reviewdb:server",
-    "//java/com/google/gwtexpui/server",
+    "//java/com/google/gerrit/util/http",
     "//lib/commons:dbcp",
     "//lib/commons:lang",
     "//lib/commons:lang3",
     "//lib/dropwizard:dropwizard-core",
+    "//lib/flogger:api",
     "//lib/guice:guice",
     "//lib/guice:guice-assistedinject",
     "//lib/guice:guice-servlet",
@@ -109,7 +110,7 @@
         "//java/com/google/gerrit/server:libserver-src.jar",
         "//java/com/google/gerrit/server/restapi:librestapi-src.jar",
         "//java/com/google/gerrit/sshd:libsshd-src.jar",
-        "//java/com/google/gwtexpui/server:libserver-src.jar",
+        "//java/com/google/gerrit/util/http:libhttp-src.jar",
     ],
 )
 
@@ -124,8 +125,8 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gwtexpui/server",
         "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/util/http",
     ],
     pkgs = ["com.google.gerrit"],
     title = "Gerrit Review Plugin API Documentation",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index c97e280..53dccff 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit c97e2806532cff00fea6424cde0d440f9ea5016d
+Subproject commit 53dccff17c029459999ff70ac886b80626af634b
diff --git a/plugins/download-commands b/plugins/download-commands
index 37219fe..cf58d79 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 37219fe3fd59727af1ac7a3b0ee00a6924ff8e00
+Subproject commit cf58d79bc034e8904aa459d8974df5796a734e1d
diff --git a/plugins/hooks b/plugins/hooks
index da73b23..c45958b 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit da73b23cfb065fc28c9e7653860ccd34bd68f0f0
+Subproject commit c45958b2c63d33643587e063730878b28c97d473
diff --git a/plugins/replication b/plugins/replication
index 5e91925..aaff48d 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 5e91925cfd391898e8e33fd149b9e1a115dafee4
+Subproject commit aaff48d82be18d99f50feab6d869cb9c98874f29
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 4672856..c73171e 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 467285664ebf8eb6f1e03ff13ebc706eee6d8662
+Subproject commit c73171ea9abbeb765d585a92753ce01151355a5c
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 45003c4..e4024e9 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 45003c4e290cd29a8695db438ca30302fa0683c7
+Subproject commit e4024e9d8d8139fc4c658c3af1a5e11e19b2d476
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 4d53631..04d8b6e 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -51,7 +51,6 @@
 
     detached() {
       this._handleHideTooltip();
-      this.unlisten(window, 'scroll', '_handleWindowScroll');
     },
 
     _setupTooltipListeners() {
@@ -59,9 +58,6 @@
       this._hasSetupTooltipListeners = true;
 
       this.addEventListener('mouseenter', this._handleShowTooltip.bind(this));
-      this.addEventListener('mouseleave', this._handleHideTooltip.bind(this));
-      this.addEventListener('tap', this._handleHideTooltip.bind(this));
-      this.listen(window, 'scroll', '_handleWindowScroll');
     },
 
     _handleShowTooltip(e) {
@@ -91,6 +87,9 @@
       tooltip.style.visibility = null;
 
       this._tooltip = tooltip;
+      this.listen(window, 'scroll', '_handleWindowScroll');
+      this.listen(this, 'mouseleave', '_handleHideTooltip');
+      this.listen(this, 'tap', '_handleHideTooltip');
     },
 
     _handleHideTooltip(e) {
@@ -100,6 +99,9 @@
         return;
       }
 
+      this.unlisten(window, 'scroll', '_handleWindowScroll');
+      this.unlisten(this, 'mouseleave', '_handleHideTooltip');
+      this.unlisten(this, 'tap', '_handleHideTooltip');
       this.setAttribute('title', this._titleText);
       if (this._tooltip && this._tooltip.parentNode) {
         this._tooltip.parentNode.removeChild(this._tooltip);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index ac12d71..704974d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -30,7 +30,7 @@
 
 <dom-module id="gr-repo">
   <template>
-    <style="shared-styles"></style>
+    <style include="shared-styles"></style>
     <style include="gr-subpage-styles">
       h2.edited:after {
         color: var(--deemphasized-text-color);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 62e9033..431d56f4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -25,6 +25,7 @@
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -61,6 +62,18 @@
         margin: 1em;
         text-align: center;
       }
+      iron-icon {
+        color: inherit;
+        height: 1.2rem;
+        margin-right: .2rem;
+        width: 1.2rem;
+      }
+      #moreActions iron-icon {
+        margin: 0;
+      }
+      .hidden {
+        display: none;
+      }
       @media screen and (max-width: 50em) {
         #mainContent,
         section,
@@ -96,14 +109,17 @@
               items="[[_topLevelPrimaryActions]]"
               as="action">
             <gr-button
+                link
                 title$="[[action.title]]"
                 has-tooltip="[[_computeHasTooltip(action.title)]]"
-                primary$="[[action.__primary]]"
                 data-action-key$="[[action.__key]]"
                 data-action-type$="[[action.__type]]"
                 data-label$="[[action.label]]"
                 disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-                on-tap="_handleActionTap">[[action.label]]</gr-button>
+                on-tap="_handleActionTap">
+                <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
+              [[action.label]]
+            </gr-button>
           </template>
         </section>
         <section id="secondaryActions"
@@ -113,27 +129,32 @@
               items="[[_topLevelSecondaryActions]]"
               as="action">
             <gr-button
+                link
                 title$="[[action.title]]"
                 has-tooltip="[[_computeHasTooltip(action.title)]]"
-                primary$="[[action.__primary]]"
                 data-action-key$="[[action.__key]]"
                 data-action-type$="[[action.__type]]"
                 data-label$="[[action.label]]"
                 disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-                on-tap="_handleActionTap">[[action.label]]</gr-button>
+                on-tap="_handleActionTap">
+              <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
+              [[action.label]]
+            </gr-button>
           </template>
         </section>
       <gr-button hidden$="[[!_loading]]" disabled>Loading actions...</gr-button>
       <gr-dropdown
           id="moreActions"
+          link
           tabindex="0"
-          down-arrow
           vertical-offset="32"
           horizontal-align="right"
           on-tap-item="_handleOveflowItemTap"
           hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
           disabled-ids="[[_disabledMenuActions]]"
-          items="[[_menuActions]]">More</gr-dropdown>
+          items="[[_menuActions]]">
+          <iron-icon icon="gr-icons:more-vert"></iron-icon>
+        </gr-dropdown>
     </div>
     <gr-overlay id="overlay" with-backdrop>
       <gr-confirm-rebase-dialog id="confirmRebase"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index cfdf88c..9104cbb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -172,6 +172,22 @@
     __type: 'change',
   };
 
+  // Set of keys that have icons. As more icons are added to gr-icons.html, this
+  // set should be expanded.
+  const ACTIONS_WITH_ICONS = new Set([
+    ChangeActions.ABANDON,
+    ChangeActions.DELETE_EDIT,
+    ChangeActions.EDIT,
+    ChangeActions.PUBLISH_EDIT,
+    ChangeActions.REBASE_EDIT,
+    ChangeActions.RESTORE,
+    ChangeActions.REVERT,
+    ChangeActions.STOP_EDIT,
+    QUICK_APPROVE_ACTION.key,
+    RevisionActions.REBASE,
+    RevisionActions.SUBMIT,
+  ]);
+
   const AWAIT_CHANGE_ATTEMPTS = 5;
   const AWAIT_CHANGE_TIMEOUT_MS = 1000;
 
@@ -1103,8 +1119,7 @@
     _setLabelValuesOnRevert(newChangeId) {
       const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
       if (!labels) { return Promise.resolve(); }
-      return this.$.restAPI.getChangeURLAndSend(newChangeId,
-          this.actions.revert.method, 'current', '/review', {labels});
+      return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels});
     },
 
     _handleResponse(action, response) {
@@ -1270,7 +1285,13 @@
 
       return revisionActionValues
           .concat(changeActionValues)
-          .sort(this._actionComparator.bind(this));
+          .sort(this._actionComparator.bind(this))
+          .map(action => {
+            if (ACTIONS_WITH_ICONS.has(action.__key)) {
+              action.icon = action.__key;
+            }
+            return action;
+          });
     },
 
     _getActionPriority(action) {
@@ -1386,5 +1407,9 @@
     _computeHasTooltip(title) {
       return !!title;
     },
+
+    _computeHasIcon(action) {
+      return action.icon ? '' : 'hidden';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 897bd21..15284ec 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -83,6 +83,9 @@
         getProjectConfig() { return Promise.resolve({}); },
       });
 
+      sandbox = sinon.sandbox.create();
+      sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+
       element = fixture('basic');
       element.change = {};
       element.changeNum = '42';
@@ -95,7 +98,6 @@
           enabled: true,
         },
       };
-      sandbox = sinon.sandbox.create();
       sandbox.stub(element.$.confirmCherrypick.$.restAPI,
           'getRepoBranches').returns(Promise.resolve([]));
       sandbox.stub(element.$.confirmMove.$.restAPI,
@@ -399,6 +401,19 @@
       assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
     });
 
+    test('_setLabelValuesOnRevert', () => {
+      const labels = {'Foo': 1, 'Bar-Baz': -2};
+      const changeId = 1234;
+      sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
+      const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
+          .returns(Promise.resolve());
+      return element._setLabelValuesOnRevert(changeId).then(() => {
+        assert.isTrue(saveStub.calledOnce);
+        assert.equal(saveStub.lastCall.args[0], changeId);
+        assert.deepEqual(saveStub.lastCall.args[2], {labels});
+      });
+    });
+
     suite('change edits', () => {
       test('shows confirm dialog for delete edit', () => {
         element.set('editMode', true);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 29ffec8..d2b5ee3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -57,6 +57,9 @@
     UNIFIED: 'UNIFIED_DIFF',
   };
 
+  const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
+  const SEND_REPLY_TIMING_LABEL = 'SendReply';
+
   Polymer({
     is: 'gr-change-view',
 
@@ -550,7 +553,9 @@
 
     _handleReplySent(e) {
       this.$.replyOverlay.close();
-      this._reload();
+      this._reload().then(() => {
+        this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
+      });
     },
 
     _handleReplyCancel(e) {
@@ -624,6 +629,8 @@
       this.$.fileList.collapseAllDiffs();
       this._patchRange = patchRange;
 
+      // If the change has already been loaded and the parameter change is only
+      // in the patch range, then don't do a full reload.
       if (this._initialLoadComplete && patchChanged) {
         if (patchRange.patchNum == null) {
           patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
@@ -637,7 +644,7 @@
       this._changeNum = value.changeNum;
       this.$.relatedChanges.clear();
 
-      this._reload().then(() => {
+      this._reload(true).then(() => {
         this._performPostLoadTasks();
       });
     },
@@ -651,7 +658,6 @@
     },
 
     _performPostLoadTasks() {
-      this.$.relatedChanges.reload();
       this._maybeShowReplyDialog();
       this._maybeShowRevertDialog();
 
@@ -1199,43 +1205,103 @@
           });
     },
 
-    _reload() {
+    /**
+     * Reload the change.
+     * @param {boolean=} opt_reloadRelatedChanges Reloads the related chanegs
+     *     when true.
+     * @return {Promise} A promise that resolves when the core data has loaded.
+     *     Some non-core data loading may still be in-flight when the core data
+     *     promise resolves.
+     */
+    _reload(opt_reloadRelatedChanges) {
       this._loading = true;
       this._relatedChangesCollapsed = true;
 
-      const detailCompletes = this._getChangeDetail().then(() => {
-        this._loading = false;
-        this._getProjectConfig();
-      });
+      // Array to house all promises related to data requests.
+      const allDataPromises = [];
 
-      this._reloadComments();
+      // Resolves when the change detail and the edit patch set (if available)
+      // are loaded.
+      const detailCompletes = this._getChangeDetail();
+      allDataPromises.push(detailCompletes);
 
-      let reloadPromise;
+      // Resolves when the loading flag is set to false, meaning that some
+      // change content may start appearing.
+      const loadingFlagSet = detailCompletes
+          .then(() => { this._loading = false; });
 
+      // Resolves when the project config has loaded.
+      const projectConfigLoaded = detailCompletes
+          .then(() => this._getProjectConfig());
+      allDataPromises.push(projectConfigLoaded);
+
+      // Resolves when change comments have loaded (comments, drafts and robot
+      // comments).
+      const commentsLoaded = this._reloadComments();
+      allDataPromises.push(commentsLoaded);
+
+      let coreDataPromise;
+
+      // If the patch number is specified
       if (this._patchRange.patchNum) {
-        reloadPromise = Promise.all([
-          this._reloadPatchNumDependentResources(),
-          detailCompletes,
-        ]).then(() => {
-          return Promise.all([
-            this._getMergeability(),
-            this.$.actions.reload(),
-          ]);
-        });
+        // Because a specific patchset is specified, reload the resources that
+        // are keyed by patch number or patch range.
+        const patchResourcesLoaded = this._reloadPatchNumDependentResources();
+        allDataPromises.push(patchResourcesLoaded);
+
+        // Promise resolves when the change detail and patch dependent resources
+        // have loaded.
+        const detailAndPatchResourcesLoaded =
+            Promise.all([patchResourcesLoaded, loadingFlagSet]);
+
+        // Promise resolves when mergeability information has loaded.
+        const mergeabilityLoaded = detailAndPatchResourcesLoaded
+            .then(() => this._getMergeability());
+        allDataPromises.push(mergeabilityLoaded);
+
+        // Promise resovles when the change actions have loaded.
+        const actionsLoaded = detailAndPatchResourcesLoaded
+            .then(() => this.$.actions.reload());
+        allDataPromises.push(actionsLoaded);
+
+        // The core data is loaded when both mergeability and actions are known.
+        coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]);
       } else {
-        // The patch number is reliant on the change detail request.
-        reloadPromise = detailCompletes.then(() => {
-          this.$.fileList.reload();
-          if (!this._latestCommitMessage) {
-            this._getLatestCommitMessage();
-          }
-          return this._getMergeability();
+        // Resolves when the file list has loaded.
+        const fileListReload = loadingFlagSet
+            .then(() => this.$.fileList.reload());
+        allDataPromises.push(fileListReload);
+
+        const latestCommitMessageLoaded = loadingFlagSet.then(() => {
+          // If the latest commit message is known, there is nothing to do.
+          if (this._latestCommitMessage) { return Promise.resolve(); }
+          return this._getLatestCommitMessage();
         });
+        allDataPromises.push(latestCommitMessageLoaded);
+
+        // Promise resolves when mergeability information has loaded.
+        const mergeabilityLoaded = loadingFlagSet
+            .then(() => this._getMergeability());
+        allDataPromises.push(mergeabilityLoaded);
+
+        // Core data is loaded when mergeability has been loaded.
+        coreDataPromise = mergeabilityLoaded;
       }
 
-      return reloadPromise.then(() => {
-        this.$.reporting.changeDisplayed();
+      if (opt_reloadRelatedChanges) {
+        const relatedChangesLoaded = coreDataPromise
+            .then(() => this.$.relatedChanges.reload());
+        allDataPromises.push(relatedChangesLoaded);
+      }
+
+      this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
+      Promise.all(allDataPromises).then(() => {
+        this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
+        this.$.reporting.changeFullyLoaded();
       });
+
+      return coreDataPromise
+          .then(() => { this.$.reporting.changeDisplayed(); });
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 3f26628..41e5227 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -312,16 +312,16 @@
             </span>
             <div class="comments desktop">
               <span class="drafts">
-                [[_computeDraftsString(changeComments, patchRange.patchNum, file.__path)]]
+                [[_computeDraftsString(changeComments, patchRange, file.__path)]]
               </span>
-              [[_computeCommentsString(changeComments, patchRange.patchNum, file.__path)]]
+              [[_computeCommentsString(changeComments, patchRange, file.__path)]]
             </div>
             <div class="comments mobile">
               <span class="drafts">
-                [[_computeDraftsStringMobile(changeComments, patchRange.patchNum,
+                [[_computeDraftsStringMobile(changeComments, patchRange,
                     file.__path)]]
               </span>
-              [[_computeCommentsStringMobile(changeComments, patchRange.patchNum,
+              [[_computeCommentsStringMobile(changeComments, patchRange,
                   file.__path)]]
             </div>
             <div class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 83f1565..e27a8ea 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -26,7 +26,10 @@
   const SIZE_BAR_GAP_WIDTH = 1;
   const SIZE_BAR_MIN_WIDTH = 1.5;
 
-  const RENDER_TIME = 'FileListRenderTime';
+  const RENDER_TIMING_LABEL = 'FileListRenderTime';
+  const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
+  const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
+  const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
 
   const FileStatus = {
     A: 'Added',
@@ -103,8 +106,6 @@
       _filesByPath: Object,
       _files: {
         type: Array,
-        computed: '_computeFiles(_filesByPath, changeComments, patchRange, ' +
-            '_reviewed)',
         observer: '_filesChanged',
         value() { return []; },
       },
@@ -148,6 +149,13 @@
         type: Array,
         computed: '_computeFilesShown(numFilesShown, _files.*)',
       },
+
+      /**
+       * The amount of files added to the shown files list the last time it was
+       * updated. This is used for reporting the average render time.
+       */
+      _reportinShownFilesIncrement: Number,
+
       _expandedFilePaths: {
         type: Array,
         value() { return []; },
@@ -183,6 +191,8 @@
 
     observers: [
       '_expandedPathsChanged(_expandedFilePaths.splices)',
+      '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
+          '_loading)',
     ],
 
     keyBindings: {
@@ -330,10 +340,9 @@
     _updateDiffPreferences() {
       if (!this.diffs.length) { return; }
       // Re-render all expanded diffs sequentially.
-      const timerName = 'Update ' + this._expandedFilePaths.length +
-          ' diffs with new prefs';
+      this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
       this._renderInOrder(this._expandedFilePaths, this.diffs,
-          this._expandedFilePaths.length, timerName);
+          this._expandedFilePaths.length);
     },
 
     _forEachDiff(fn) {
@@ -372,14 +381,17 @@
      * Computes a string with the number of comments and unresolved comments.
      *
      * @param {!Object} changeComments
-     * @param {number} patchNum
+     * @param {!Object} patchRange
      * @param {string} path
      * @return {string}
      */
-    _computeCommentsString(changeComments, patchNum, path) {
-      const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
-          path);
-      const commentCount = changeComments.computeCommentCount(patchNum, path);
+    _computeCommentsString(changeComments, patchRange, path) {
+      const unresolvedCount =
+          changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) +
+          changeComments.computeUnresolvedNum(patchRange.patchNum, path);
+      const commentCount =
+          changeComments.computeCommentCount(patchRange.basePatchNum, path) +
+          changeComments.computeCommentCount(patchRange.patchNum, path);
       const commentString = GrCountStringFormatter.computePluralString(
           commentCount, 'comment');
       const unresolvedString = GrCountStringFormatter.computeString(
@@ -396,12 +408,14 @@
      * Computes a string with the number of drafts.
      *
      * @param {!Object} changeComments
-     * @param {number} patchNum
+     * @param {!Object} patchRange
      * @param {string} path
      * @return {string}
      */
-    _computeDraftsString(changeComments, patchNum, path) {
-      const draftCount = changeComments.computeDraftCount(patchNum, path);
+    _computeDraftsString(changeComments, patchRange, path) {
+      const draftCount =
+          changeComments.computeDraftCount(patchRange.basePatchNum, path) +
+          changeComments.computeDraftCount(patchRange.patchNum, path);
       return GrCountStringFormatter.computePluralString(draftCount, 'draft');
     },
 
@@ -409,12 +423,14 @@
      * Computes a shortened string with the number of drafts.
      *
      * @param {!Object} changeComments
-     * @param {number} patchNum
+     * @param {!Object} patchRange
      * @param {string} path
      * @return {string}
      */
-    _computeDraftsStringMobile(changeComments, patchNum, path) {
-      const draftCount = changeComments.computeDraftCount(patchNum, path);
+    _computeDraftsStringMobile(changeComments, patchRange, path) {
+      const draftCount =
+          changeComments.computeDraftCount(patchRange.basePatchNum, path) +
+          changeComments.computeDraftCount(patchRange.patchNum, path);
       return GrCountStringFormatter.computeShortString(draftCount, 'd');
     },
 
@@ -422,12 +438,14 @@
      * Computes a shortened string with the number of comments.
      *
      * @param {!Object} changeComments
-     * @param {number} patchNum
+     * @param {!Object} patchRange
      * @param {string} path
      * @return {string}
      */
-    _computeCommentsStringMobile(changeComments, patchNum, path) {
-      const commentCount = changeComments.computeCommentCount(patchNum, path);
+    _computeCommentsStringMobile(changeComments, patchRange, path) {
+      const commentCount =
+          changeComments.computeCommentCount(patchRange.basePatchNum, path) +
+          changeComments.computeCommentCount(patchRange.patchNum, path);
       return GrCountStringFormatter.computeShortString(commentCount, 'c');
     },
 
@@ -782,13 +800,14 @@
           'gr-icons:expand-less' : 'gr-icons:expand-more';
     },
 
-    _computeFiles(filesByPath, changeComments, patchRange, reviewed) {
+    _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
+      // Await all promises resolving from reload. @See Issue 9057
+      if (loading) { return; }
+
       const commentedPaths = changeComments.getPaths(patchRange);
       const files = Object.assign({}, filesByPath);
       Object.keys(commentedPaths).forEach(commentedPath => {
-        if (files.hasOwnProperty(commentedPath)) {
-          return;
-        }
+        if (files.hasOwnProperty(commentedPath)) { return; }
         files[commentedPath] = {status: 'U'};
       });
       const reviewedSet = new Set(reviewed || []);
@@ -797,24 +816,30 @@
         files[filePath].isReviewed = reviewedSet.has(filePath);
       }
 
-      return this._normalizeChangeFilesResponse(files);
+      this._files = this._normalizeChangeFilesResponse(files);
     },
 
     _computeFilesShown(numFilesShown, files) {
+      const previousNumFilesShown = this._shownFiles ?
+          this._shownFiles.length : 0;
+
       const filesShown = files.base.slice(0, numFilesShown);
       this.fire('files-shown-changed', {length: filesShown.length});
 
       // Start the timer for the rendering work hwere because this is where the
       // _shownFiles property is being set, and _shownFiles is used in the
       // dom-repeat binding.
-      this.$.reporting.time(RENDER_TIME);
+      this.$.reporting.time(RENDER_TIMING_LABEL);
+
+      // How many more files are being shown (if it's an increase).
+      this._reportinShownFilesIncrement =
+          Math.max(0, filesShown.length - previousNumFilesShown);
 
       return filesShown;
     },
 
     _updateDiffCursor() {
       const diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
-
       // Overwrite the cursor's list of diffs:
       this.$.diffCursor.splice(
           ...['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements));
@@ -905,26 +930,26 @@
           this._expandedFilePaths.indexOf(diff.path) === -1);
       this._clearCollapsedDiffs(collapsedDiffs);
 
-      if (!record) { return; }
+      if (!record) { return; } // Happens after "Collapse all" clicked.
 
       this.filesExpanded = this._computeExpandedFiles(
           this._expandedFilePaths.length, this._files.length);
 
       // Find the paths introduced by the new index splices:
       const newPaths = record.indexSplices
-          .map(splice => {
-            return splice.object.slice(splice.index,
-                splice.index + splice.addedCount);
-          })
-          .reduce((acc, paths) => { return acc.concat(paths); }, []);
-
-      const timerName = 'Expand ' + newPaths.length + ' diffs';
-      this.$.reporting.time(timerName);
+            .map(splice => splice.object.slice(
+                splice.index, splice.index + splice.addedCount))
+            .reduce((acc, paths) => acc.concat(paths), []);
 
       // Required so that the newly created diff view is included in this.diffs.
       Polymer.dom.flush();
 
-      this._renderInOrder(newPaths, this.diffs, newPaths.length, timerName);
+      this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+
+      if (newPaths.length) {
+        this._renderInOrder(newPaths, this.diffs, newPaths.length);
+      }
+
       this._updateDiffCursor();
       this.$.diffCursor.handleDiffUpdate();
     },
@@ -944,11 +969,9 @@
      * @param  {!NodeList<!Object>} diffElements (GrDiffElement)
      * @param  {number} initialCount The total number of paths in the pass. This
      *   is used to generate log messages.
-     * @param {string} timerName the timer to stop after the render has
-     *   completed
      * @return {!Promise}
      */
-    _renderInOrder(paths, diffElements, initialCount, timerName) {
+    _renderInOrder(paths, diffElements, initialCount) {
       let iter = 0;
 
       return (new Promise(resolve => {
@@ -972,7 +995,8 @@
           this._cancelForEachDiff = null;
           this._nextRenderParams = null;
           console.log('Finished expanding', initialCount, 'diff(s)');
-          this.$.reporting.timeEnd(timerName);
+          this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
+              EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
           this.$.diffCursor.handleDiffUpdate();
         });
       });
@@ -1198,7 +1222,8 @@
     _reportRenderedRow(index) {
       if (index === this._shownFiles.length - 1) {
         this.async(() => {
-          this.$.reporting.timeEnd(RENDER_TIME);
+          this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
+              RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
         }, 1);
       }
       return '';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 3c90a1f..5833a9a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -60,9 +60,11 @@
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(true); },
         getPreferences() { return Promise.resolve({}); },
+        getDiffPreferences() { return Promise.resolve({}); },
         getDiffComments() { return Promise.resolve({}); },
         getDiffRobotComments() { return Promise.resolve({}); },
         getDiffDrafts() { return Promise.resolve({}); },
+        getAccountCapabilities() { return Promise.resolve({}); },
       });
       stub('gr-date-formatter', {
         _loadTimeFormat() { return Promise.resolve(''); },
@@ -85,6 +87,7 @@
             .returns({meta: {}, left: [], right: []});
         done();
       });
+      element._loading = false;
       element.diffPrefs = {};
       element.numFilesShown = 200;
       element.patchRange = {
@@ -388,68 +391,147 @@
         ],
       };
 
+      const parentTo1 = {
+        basePatchNum: 'PARENT',
+        patchNum: '1',
+      };
+
+      const parentTo2 = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
+
+      const _1To2 = {
+        basePatchNum: '1',
+        patchNum: '2',
+      };
+
       assert.equal(
-          element._computeCommentsString(element.changeComments, '1',
+          element._computeCommentsString(element.changeComments, parentTo1,
               '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)');
       assert.equal(
-          element._computeCommentsStringMobile(element.changeComments, '1'
+          element._computeCommentsString(element.changeComments, _1To2,
+              '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
+      assert.equal(
+          element._computeCommentsStringMobile(element.changeComments, parentTo1
           , '/COMMIT_MSG'), '2c');
       assert.equal(
-          element._computeDraftsString(element.changeComments, '1',
+          element._computeCommentsStringMobile(element.changeComments, _1To2
+          , '/COMMIT_MSG'), '3c');
+      assert.equal(
+          element._computeDraftsString(element.changeComments, parentTo1,
               'unresolved.file'), '1 draft');
       assert.equal(
-          element._computeDraftsStringMobile(element.changeComments, '1',
+          element._computeDraftsString(element.changeComments, _1To2,
+              'unresolved.file'), '1 draft');
+      assert.equal(
+          element._computeDraftsStringMobile(element.changeComments, parentTo1,
               'unresolved.file'), '1d');
       assert.equal(
-          element._computeCommentsString(element.changeComments, '1',
+          element._computeDraftsStringMobile(element.changeComments, _1To2,
+              'unresolved.file'), '1d');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, parentTo1,
               'myfile.txt', 'comment'), '1 comment');
       assert.equal(
-          element._computeCommentsStringMobile(element.changeComments, '1',
+          element._computeCommentsString(element.changeComments, _1To2,
+              'myfile.txt', 'comment'), '3 comments');
+      assert.equal(
+          element._computeCommentsStringMobile(element.changeComments, parentTo1,
               'myfile.txt'), '1c');
       assert.equal(
-          element._computeDraftsString(element.changeComments, '1',
+          element._computeCommentsStringMobile(element.changeComments, _1To2,
+              'myfile.txt'), '3c');
+      assert.equal(
+          element._computeDraftsString(element.changeComments, parentTo1,
               'myfile.txt'), '');
       assert.equal(
-          element._computeDraftsStringMobile(element.changeComments, '1',
+          element._computeDraftsString(element.changeComments, _1To2,
               'myfile.txt'), '');
       assert.equal(
-          element._computeCommentsString(element.changeComments, '1',
+          element._computeDraftsStringMobile(element.changeComments, parentTo1,
+              'myfile.txt'), '');
+      assert.equal(
+          element._computeDraftsStringMobile(element.changeComments, _1To2,
+              'myfile.txt'), '');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, parentTo1,
               'file_added_in_rev2.txt', 'comment'), '');
       assert.equal(
-          element._computeCommentsStringMobile(element.changeComments, '1',
+          element._computeCommentsString(element.changeComments, _1To2,
+              'file_added_in_rev2.txt', 'comment'), '');
+      assert.equal(
+          element._computeCommentsStringMobile(element.changeComments, parentTo1,
               'file_added_in_rev2.txt'), '');
       assert.equal(
-          element._computeDraftsString(element.changeComments, '1',
+          element._computeCommentsStringMobile(element.changeComments, _1To2,
               'file_added_in_rev2.txt'), '');
       assert.equal(
-          element._computeDraftsStringMobile(element.changeComments, '1',
+          element._computeDraftsString(element.changeComments, parentTo1,
               'file_added_in_rev2.txt'), '');
       assert.equal(
-          element._computeCommentsString(element.changeComments, '2',
+          element._computeDraftsString(element.changeComments, _1To2,
+              'file_added_in_rev2.txt'), '');
+      assert.equal(
+          element._computeDraftsStringMobile(element.changeComments, parentTo1,
+              'file_added_in_rev2.txt'), '');
+      assert.equal(
+          element._computeDraftsStringMobile(element.changeComments, _1To2,
+              'file_added_in_rev2.txt'), '');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, parentTo2,
               '/COMMIT_MSG', 'comment'), '1 comment');
       assert.equal(
-          element._computeCommentsStringMobile(element.changeComments, '2',
+          element._computeCommentsString(element.changeComments, _1To2,
+              '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
+      assert.equal(
+          element._computeCommentsStringMobile(element.changeComments, parentTo2,
               '/COMMIT_MSG'), '1c');
       assert.equal(
-          element._computeDraftsString(element.changeComments, '1',
+          element._computeCommentsStringMobile(element.changeComments, _1To2,
+              '/COMMIT_MSG'), '3c');
+      assert.equal(
+          element._computeDraftsString(element.changeComments, parentTo1,
               '/COMMIT_MSG'), '2 drafts');
       assert.equal(
-          element._computeDraftsStringMobile(element.changeComments, '1',
+          element._computeDraftsString(element.changeComments, _1To2,
+              '/COMMIT_MSG'), '2 drafts');
+      assert.equal(
+          element._computeDraftsStringMobile(element.changeComments, parentTo1,
               '/COMMIT_MSG'), '2d');
       assert.equal(
-          element._computeCommentsString(element.changeComments, '2',
+          element._computeDraftsStringMobile(element.changeComments, _1To2,
+              '/COMMIT_MSG'), '2d');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, parentTo2,
               'myfile.txt', 'comment'), '2 comments');
       assert.equal(
-          element._computeCommentsStringMobile(element.changeComments, '2',
+          element._computeCommentsString(element.changeComments, _1To2,
+              'myfile.txt', 'comment'), '3 comments');
+      assert.equal(
+          element._computeCommentsStringMobile(element.changeComments, parentTo2,
               'myfile.txt'), '2c');
       assert.equal(
-          element._computeDraftsStringMobile(element.changeComments, '2',
+          element._computeCommentsStringMobile(element.changeComments, _1To2,
+              'myfile.txt'), '3c');
+      assert.equal(
+          element._computeDraftsStringMobile(element.changeComments, parentTo2,
               'myfile.txt'), '');
       assert.equal(
-          element._computeCommentsString(element.changeComments, '2',
+          element._computeDraftsStringMobile(element.changeComments, _1To2,
+              'myfile.txt'), '');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, parentTo2,
               'file_added_in_rev2.txt', 'comment'), '');
-      assert.equal(element._computeCommentsString(element.changeComments, '2',
-          'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, _1To2,
+              'file_added_in_rev2.txt', 'comment'), '');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, parentTo2,
+              'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+      assert.equal(
+          element._computeCommentsString(element.changeComments, _1To2,
+              'unresolved.file', 'comment'), '3 comments (1 unresolved)');
     });
 
     suite('keyboard shortcuts', () => {
@@ -880,7 +962,7 @@
       flushAsynchronousOperations();
 
       assert.equal(element.$$('iron-icon').icon, 'gr-icons:expand-more');
-      assert.equal(renderSpy.callCount, 2);
+      assert.equal(renderSpy.callCount, 1);
       assert.notInclude(element._expandedFilePaths, path);
       assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
@@ -1321,6 +1403,7 @@
             .returns({meta: {}, left: [], right: []});
         done();
       });
+      element._loading = false;
       element.numFilesShown = 75;
       element.selectedIndex = 0;
       element._filesByPath = {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index a6e1b40..00157ab 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -16,6 +16,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-icon/iron-icon.html">
 <link rel="import" href="../../shared/gr-account-label/gr-account-label.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
@@ -115,7 +116,7 @@
         overflow: hidden;
         text-overflow: ellipsis;
       }
-      .collapsed .date {
+      .collapsed .dateContainer {
         position: static;
       }
       .collapsed .author {
@@ -126,12 +127,17 @@
         cursor: pointer;
         margin-bottom: .4em;
       }
-      .date {
-        color: var(--deemphasized-text-color);
+      .dateContainer {
         position: absolute;
         right: var(--default-horizontal-margin);
         top: 10px;
       }
+      .date {
+        color: var(--deemphasized-text-color);
+      }
+      .dateContainer iron-icon {
+        cursor: pointer;
+      }
       .replyContainer {
         padding: .5em 0 0 0;
       }
@@ -211,22 +217,29 @@
             </template>
           </div>
         </template>
-        <template is="dom-if" if="[[!message.id]]">
-          <span class="date">
-            <gr-date-formatter
-                has-tooltip
-                show-date-and-time
-                date-str="[[message.date]]"></gr-date-formatter>
-          </span>
-        </template>
-        <template is="dom-if" if="[[message.id]]">
-          <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
-            <gr-date-formatter
-                has-tooltip
-                show-date-and-time
-                date-str="[[message.date]]"></gr-date-formatter>
-          </a>
-        </template>
+        <span class="dateContainer">
+          <template is="dom-if" if="[[!message.id]]">
+            <span class="date">
+              <gr-date-formatter
+                  has-tooltip
+                  show-date-and-time
+                  date-str="[[message.date]]"></gr-date-formatter>
+            </span>
+          </template>
+          <template is="dom-if" if="[[message.id]]">
+            <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
+              <gr-date-formatter
+                  has-tooltip
+                  show-date-and-time
+                  date-str="[[message.date]]"></gr-date-formatter>
+            </a>
+          </template>
+          <iron-icon
+              id="expandToggle"
+              on-tap="_toggleExpanded"
+              title="Toggle expanded state"
+              icon="[[_computeExpandToggleIcon(_expanded)]]">
+        </span>
       </div>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index a6d4b03..0590c73 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -251,5 +251,14 @@
         this._projectConfig = config;
       });
     },
+
+    _computeExpandToggleIcon(expanded) {
+      return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+    },
+
+    _toggleExpanded(e) {
+      e.stopPropagation();
+      this.set('message.expanded', !this.message.expanded);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index df39362..c9f0a16 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
@@ -309,6 +310,7 @@
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-reply-dialog.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 97e7e78..65d681d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -54,6 +54,8 @@
 
   const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
 
+  const SEND_REPLY_TIMING_LABEL = 'SendReply';
+
   Polymer({
     is: 'gr-reply-dialog',
 
@@ -429,6 +431,7 @@
     },
 
     send(includeComments, startReview) {
+      this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
       const labels = this.$.labelScores.getLabelValues();
 
       const obj = {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 8ce59f2..04e3794 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
@@ -101,25 +102,37 @@
           color: var(--primary-text-color);
         }
       }
+      .settingsButton {
+        margin-left: .5em;
+      }
       .browse {
         color: var(--header-text-color);
         /* Same as gr-button */
         margin: 5px 4px;
         text-decoration: none;
       }
-      .accountContainer:not(.loggedIn):not(.loggedOut) .loginButton,
-      .accountContainer:not(.loggedIn):not(.loggedOut) gr-account-dropdown,
-      .accountContainer.loggedIn .loginButton,
-      .accountContainer.loggedOut gr-account-dropdown {
+      .settingsButton,
+      gr-account-dropdown {
         display: none;
       }
+      :host([loading]) .accountContainer,
+      :host([logged-in]) .loginButton {
+        display: none;
+      }
+      :host([logged-in]) .settingsButton,
+      :host([logged-in]) gr-account-dropdown {
+        display: inline;
+      }
+      iron-icon {
+        color: var(--header-text-color);
+      }
       .accountContainer {
         align-items: center;
         display: flex;
         margin: 0 -.5em 0 .5em;
-        white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
+        white-space: nowrap;
       }
       .loginButton {
         color: var(--header-text-color);
@@ -181,6 +194,12 @@
             name="header-browse-source"></gr-endpoint-decorator>
         <div class="accountContainer" id="accountContainer">
           <a class="loginButton" href$="[[_loginURL]]">Sign in</a>
+          <a
+              class="settingsButton"
+              href$="[[_generateSettingsLink()]]"
+              title="Settings">
+            <iron-icon icon="gr-icons:settings"></iron-icon>
+          </a>
           <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 42c744f..dad7b7c 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -74,6 +74,14 @@
         type: String,
         notify: true,
       },
+      loggedIn: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
+      loading: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
 
       /** @type {?Object} */
       _account: Object,
@@ -192,6 +200,7 @@
     },
 
     _loadAccount() {
+      this.loading = true;
       const promises = [
         this.$.restAPI.getAccount(),
         Gerrit.awaitPluginsLoaded(),
@@ -200,8 +209,8 @@
       return Promise.all(promises).then(result => {
         const account = result[0];
         this._account = account;
-        this.$.accountContainer.classList.toggle('loggedIn', account != null);
-        this.$.accountContainer.classList.toggle('loggedOut', account == null);
+        this.loggedIn = !!account;
+        this.loading = false;
 
         return this.getAdminLinks(account,
             this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
@@ -254,5 +263,9 @@
       // Groups are not yet supported.
       return !linkObj.url.startsWith('/groups');
     },
+
+    _generateSettingsLink() {
+      return this.getBaseUrl() + '/settings/';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 5d51546..30e8e1f 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -53,6 +53,30 @@
       sandbox.restore();
     });
 
+    test('link visibility', () => {
+      element.loading = true;
+      assert.equal(getComputedStyle(element.$$('.accountContainer')).display,
+          'none');
+      element.loading = false;
+      element.loggedIn = false;
+      assert.notEqual(getComputedStyle(element.$$('.accountContainer')).display,
+          'none');
+      assert.notEqual(getComputedStyle(element.$$('.loginButton')).display,
+          'none');
+      assert.equal(getComputedStyle(element.$$('gr-account-dropdown')).display,
+          'none');
+      assert.equal(getComputedStyle(element.$$('.settingsButton')).display,
+          'none');
+      element.loggedIn = true;
+      assert.equal(getComputedStyle(element.$$('.loginButton')).display,
+          'none');
+      assert.notEqual(getComputedStyle(element.$$('gr-account-dropdown'))
+          .display,
+          'none');
+      assert.notEqual(getComputedStyle(element.$$('.settingsButton')).display,
+          'none');
+    });
+
     test('fix my menu item', () => {
       assert.deepEqual([
         {url: 'https://awesometown.com/#hashyhash'},
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index cbb2c09..935de6b 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -16,7 +16,6 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-reporting">
   <script src="gr-jank-detector.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index ae67dac..feff01d 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -70,11 +70,13 @@
 
   const TIMER = {
     CHANGE_DISPLAYED: 'ChangeDisplayed',
+    CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
     DASHBOARD_DISPLAYED: 'DashboardDisplayed',
     DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
     FILE_LIST_DISPLAYED: 'FileListDisplayed',
     PLUGINS_LOADED: 'PluginsLoaded',
     STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
+    STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
     STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
     STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
     STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
@@ -84,6 +86,7 @@
   const STARTUP_TIMERS = {};
   STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
+  STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
   STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
@@ -148,8 +151,13 @@
       return window.performance.now();
     },
 
+    _arePluginsLoaded() {
+      return this._baselines &&
+        !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
+    },
+
     reporter(...args) {
-      const report = (Gerrit._arePluginsLoaded() && !pending.length) ?
+      const report = (this._arePluginsLoaded() && !pending.length) ?
         this.defaultReporter : this.cachingReporter;
       report.apply(this, args);
     },
@@ -174,7 +182,7 @@
       if (type === ERROR.TYPE) {
         console.error(eventValue.error || eventName);
       }
-      if (Gerrit._arePluginsLoaded()) {
+      if (this._arePluginsLoaded()) {
         if (pending.length) {
           for (const args of pending.splice(0)) {
             this.reporter(...args);
@@ -225,6 +233,7 @@
         delete this._baselines[prop];
       }
       this.time(TIMER.CHANGE_DISPLAYED);
+      this.time(TIMER.CHANGE_LOAD_FULL);
       this.time(TIMER.DASHBOARD_DISPLAYED);
       this.time(TIMER.DIFF_VIEW_DISPLAYED);
       this.time(TIMER.FILE_LIST_DISPLAYED);
@@ -251,6 +260,14 @@
       }
     },
 
+    changeFullyLoaded() {
+      if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
+        this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
+      } else {
+        this.timeEnd(TIMER.CHANGE_LOAD_FULL);
+      }
+    },
+
     diffViewDisplayed() {
       if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
         this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED);
@@ -295,6 +312,26 @@
       delete this._baselines[name];
     },
 
+    /**
+     * Reports just line timeEnd, but additionally reports an average given a
+     * denominator and a separate reporiting name for the average.
+     * @param {string} name Timing name.
+     * @param {string} averageName Average timing name.
+     * @param {number} denominator Number by which to divide the total to
+     *     compute the average.
+     */
+    timeEndWithAverage(name, averageName, denominator) {
+      if (!this._baselines.hasOwnProperty(name)) { return; }
+      const baseTime = this._baselines[name];
+      this.timeEnd(name);
+
+      // Guard against division by zero.
+      if (!denominator) { return; }
+      const time = Math.round(this.now() - baseTime);
+      this.reporter(TIMING.TYPE, TIMING.CATEGORY, averageName,
+          Math.round(time / denominator));
+    },
+
     reportInteraction(eventName, opt_msg) {
       this.reporter(INTERACTION_TYPE, this.category, eventName, opt_msg);
     },
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index e2bb83d..3c6d4bf 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -100,6 +100,7 @@
           'lifecycle', 'UI Latency', 'Jank count', 42));
       assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
       assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
+      assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
       assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
       assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
       assert.isFalse(element._baselines.hasOwnProperty('garbage'));
@@ -116,6 +117,17 @@
       assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed'));
     });
 
+    test('changeFullyLoaded', () => {
+      sandbox.spy(element, 'timeEnd');
+      element.changeFullyLoaded();
+      assert.isFalse(
+          element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+      assert.isTrue(
+          element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
+      element.changeFullyLoaded();
+      assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+    });
+
     test('diffViewDisplayed', () => {
       sandbox.spy(element, 'timeEnd');
       element.diffViewDisplayed();
@@ -166,6 +178,19 @@
       ));
     });
 
+    test('timeEndWithAverage', () => {
+      const nowStub = sandbox.stub(element, 'now').returns(0);
+      nowStub.returns(1000);
+      element.time('foo');
+      nowStub.returns(1100);
+      element.timeEndWithAverage('foo', 'bar', 10);
+      assert.isTrue(element.reporter.calledTwice);
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing-report', 'UI Latency', 'foo', 100));
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing-report', 'UI Latency', 'bar', 10));
+    });
+
     test('reportExtension', () => {
       element.reportExtension('foo');
       assert.isTrue(element.reporter.calledWithExactly(
@@ -177,11 +202,9 @@
       setup(() => {
         element.reporter.restore();
         sandbox.stub(element, 'defaultReporter');
-        sandbox.stub(Gerrit, '_arePluginsLoaded');
       });
 
       test('pluginsLoaded reports time', () => {
-        Gerrit._arePluginsLoaded.returns(true);
         sandbox.stub(element, 'now').returns(42);
         element.pluginsLoaded();
         assert.isTrue(element.defaultReporter.calledWithExactly(
@@ -190,21 +213,18 @@
       });
 
       test('pluginsLoaded reports plugins', () => {
-        Gerrit._arePluginsLoaded.returns(true);
         element.pluginsLoaded(['foo', 'bar']);
-        assert.isTrue(element.defaultReporter.calledWithExactly(
+        assert.isTrue(element.defaultReporter.calledWith(
             'lifecycle', 'Plugins installed', 'foo,bar'
         ));
       });
 
       test('caches reports if plugins are not loaded', () => {
-        Gerrit._arePluginsLoaded.returns(false);
         element.timeEnd('foo');
         assert.isFalse(element.defaultReporter.called);
       });
 
       test('reports if plugins are loaded', () => {
-        Gerrit._arePluginsLoaded.returns(true);
         element.pluginsLoaded();
         assert.isTrue(element.defaultReporter.called);
       });
@@ -212,14 +232,19 @@
       test('reports cached events preserving order', () => {
         element.time('foo');
         element.time('bar');
-        Gerrit._arePluginsLoaded.returns(false);
         element.timeEnd('foo');
-        Gerrit._arePluginsLoaded.returns(true);
+        element.pluginsLoaded();
         element.timeEnd('bar');
-        assert.isTrue(element.defaultReporter.firstCall.calledWith(
+        assert.isTrue(element.defaultReporter.getCall(0).calledWith(
             'timing-report', 'UI Latency', 'foo'
         ));
-        assert.isTrue(element.defaultReporter.secondCall.calledWith(
+        assert.isTrue(element.defaultReporter.getCall(1).calledWith(
+            'timing-report', 'UI Latency', 'PluginsLoaded'
+        ));
+        assert.isTrue(element.defaultReporter.getCall(2).calledWith(
+            'lifecycle', 'Plugins installed'
+        ));
+        assert.isTrue(element.defaultReporter.getCall(3).calledWith(
             'timing-report', 'UI Latency', 'bar'
         ));
       });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 8af7301..6adc286 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -113,16 +113,21 @@
     CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
 
     // Matches
-    // /c/<project>/+/<changeNum>/
-    //     [<basePatchNum|edit>..][<patchNum|edit>]/[path].
+    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
     // TODO(kaspern): Migrate completely to project based URLs, with backwards
     // compatibility for change-only.
-    // eslint-disable-next-line max-len
-    CHANGE_OR_DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))?))?\/?$/,
+    CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
 
     // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
     CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
 
+    // Matches
+    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
+    // TODO(kaspern): Migrate completely to project based URLs, with backwards
+    // compatibility for change-only.
+    // eslint-disable-next-line max-len
+    DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
+
     // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit
     DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit$/,
 
@@ -642,24 +647,13 @@
         return;
       }
       page(pattern, this._loadUserMiddleware.bind(this), data => {
-        this.$.reporting.locationChanged(this._getPageName(handlerName, data));
+        this.$.reporting.locationChanged(handlerName);
         const promise = opt_authRedirect ?
           this._redirectIfNotLoggedIn(data) : Promise.resolve();
         promise.then(() => { this[handlerName](data); });
       });
     },
 
-    _getPageName(handlerName, ctx) {
-      switch (handlerName) {
-        case '_handleChangeOrDiffRoute': {
-          const isDiffView = ctx.params[8];
-          return isDiffView ? Gerrit.Nav.View.DIFF : Gerrit.Nav.View.CHANGE;
-        }
-        default:
-          return handlerName;
-      }
-    },
-
     _startRouter() {
       const base = this.getBaseUrl();
       if (base) {
@@ -806,7 +800,9 @@
 
       this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
 
-      this._mapRoute(RoutePattern.CHANGE_OR_DIFF, '_handleChangeOrDiffRoute');
+      this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
+
+      this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
 
       this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
 
@@ -1245,9 +1241,20 @@
       this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
     },
 
-    _handleChangeOrDiffRoute(ctx) {
-      const isDiffView = ctx.params[8];
+    _handleChangeRoute(ctx) {
+      // Parameter order is based on the regex group number matched.
+      const params = {
+        project: ctx.params[0],
+        changeNum: ctx.params[1],
+        basePatchNum: ctx.params[4],
+        patchNum: ctx.params[6],
+        view: Gerrit.Nav.View.CHANGE,
+      };
 
+      this._redirectOrNavigate(params);
+    },
+
+    _handleDiffRoute(ctx) {
       // Parameter order is based on the regex group number matched.
       const params = {
         project: ctx.params[0],
@@ -1255,15 +1262,13 @@
         basePatchNum: ctx.params[4],
         patchNum: ctx.params[6],
         path: ctx.params[8],
-        view: isDiffView ? Gerrit.Nav.View.DIFF : Gerrit.Nav.View.CHANGE,
+        view: Gerrit.Nav.View.DIFF,
       };
 
-      if (isDiffView) {
-        const address = this._parseLineAddress(ctx.hash);
-        if (address) {
-          params.leftSide = address.leftSide;
-          params.lineNum = address.lineNum;
-        }
+      const address = this._parseLineAddress(ctx.hash);
+      if (address) {
+        params.leftSide = address.leftSide;
+        params.lineNum = address.lineNum;
       }
 
       this._redirectOrNavigate(params);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index e0a7e46..b68a5e9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -152,7 +152,8 @@
         '_handleBranchListFilterRoute',
         '_handleBranchListOffsetRoute',
         '_handleChangeNumberLegacyRoute',
-        '_handleChangeOrDiffRoute',
+        '_handleChangeRoute',
+        '_handleDiffRoute',
         '_handleDefaultRoute',
         '_handleChangeLegacyRoute',
         '_handleDiffLegacyRoute',
@@ -1267,7 +1268,57 @@
               '/c/1234/3..8/foo/bar#b123'));
         });
 
-        suite('_handleChangeOrDiffRoute', () => {
+        suite('_handleChangeRoute', () => {
+          let normalizeRangeStub;
+
+          function makeParams(path, hash) {
+            return {
+              params: [
+                'foo/bar', // 0 Project
+                1234, // 1 Change number
+                null, // 2 Unused
+                null, // 3 Unused
+                4, // 4 Base patch number
+                null, // 5 Unused
+                7, // 6 Patch number
+              ],
+            };
+          }
+
+          setup(() => {
+            normalizeRangeStub = sandbox.stub(element,
+                '_normalizePatchRangeParams');
+            sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+          });
+
+          test('needs redirect', () => {
+            normalizeRangeStub.returns(true);
+            sandbox.stub(element, '_generateUrl').returns('foo');
+            const ctx = makeParams(null, '');
+            element._handleChangeRoute(ctx);
+            assert.isTrue(normalizeRangeStub.called);
+            assert.isFalse(setParamsStub.called);
+            assert.isTrue(redirectStub.calledOnce);
+            assert.isTrue(redirectStub.calledWithExactly('foo'));
+          });
+
+          test('change view', () => {
+            normalizeRangeStub.returns(false);
+            sandbox.stub(element, '_generateUrl').returns('foo');
+            const ctx = makeParams(null, '');
+            assertDataToParams(ctx, '_handleChangeRoute', {
+              view: Gerrit.Nav.View.CHANGE,
+              project: 'foo/bar',
+              changeNum: 1234,
+              basePatchNum: 4,
+              patchNum: 7,
+            });
+            assert.isFalse(redirectStub.called);
+            assert.isTrue(normalizeRangeStub.called);
+          });
+        });
+
+        suite('_handleDiffRoute', () => {
           let normalizeRangeStub;
 
           function makeParams(path, hash) {
@@ -1297,40 +1348,18 @@
             normalizeRangeStub.returns(true);
             sandbox.stub(element, '_generateUrl').returns('foo');
             const ctx = makeParams(null, '');
-            element._handleChangeOrDiffRoute(ctx);
+            element._handleDiffRoute(ctx);
             assert.isTrue(normalizeRangeStub.called);
             assert.isFalse(setParamsStub.called);
             assert.isTrue(redirectStub.calledOnce);
             assert.isTrue(redirectStub.calledWithExactly('foo'));
           });
 
-          test('change view', () => {
-            normalizeRangeStub.returns(false);
-            sandbox.stub(element, '_generateUrl').returns('foo');
-            const ctx = makeParams(null, '');
-            assertDataToParams(ctx, '_handleChangeOrDiffRoute', {
-              view: Gerrit.Nav.View.CHANGE,
-              project: 'foo/bar',
-              changeNum: 1234,
-              basePatchNum: 4,
-              patchNum: 7,
-              path: null,
-            });
-            assert.isFalse(redirectStub.called);
-            assert.isTrue(normalizeRangeStub.called);
-          });
-
-          test('gr-reporting recognizes change page', () => {
-            const ctx = makeParams(null, '');
-            assert.equal(element._getPageName('_handleChangeOrDiffRoute', ctx),
-                Gerrit.Nav.View.CHANGE);
-          });
-
           test('diff view', () => {
             normalizeRangeStub.returns(false);
             sandbox.stub(element, '_generateUrl').returns('foo');
             const ctx = makeParams('foo/bar/baz', 'b44');
-            assertDataToParams(ctx, '_handleChangeOrDiffRoute', {
+            assertDataToParams(ctx, '_handleDiffRoute', {
               view: Gerrit.Nav.View.DIFF,
               project: 'foo/bar',
               changeNum: 1234,
@@ -1343,12 +1372,6 @@
             assert.isFalse(redirectStub.called);
             assert.isTrue(normalizeRangeStub.called);
           });
-
-          test('gr-reporting recognizes diff page', () => {
-            const ctx = makeParams('foo/bar/baz', 'b44');
-            assert.equal(element._getPageName('_handleChangeOrDiffRoute', ctx),
-                Gerrit.Nav.View.DIFF);
-          });
         });
 
         test('_handleDiffEditRoute', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 9f07d90..27f8d39 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -34,13 +34,42 @@
       GrDiffBuilderSideBySide.prototype);
   GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
 
-  GrDiffBuilderImage.prototype.renderDiffImages = function() {
+  GrDiffBuilderImage.prototype.renderDiff = function() {
     const section = this._createElement('tbody', 'image-diff');
 
     this._emitImagePair(section);
     this._emitImageLabels(section);
 
     this._outputEl.appendChild(section);
+    this._outputEl.appendChild(this._createEndpoint());
+  };
+
+  GrDiffBuilderImage.prototype._createEndpoint = function() {
+    const tbody = this._createElement('tbody');
+    const tr = this._createElement('tr');
+    const td = this._createElement('td');
+
+    // TODO(kaspern): Support blame for image diffs and remove the hardcoded 4
+    // column limit.
+    td.setAttribute('colspan', '4');
+    const endpoint = this._createElement('gr-endpoint-decorator');
+    const endpointDomApi = Polymer.dom(endpoint);
+    endpointDomApi.setAttribute('name', 'image-diff');
+    endpointDomApi.appendChild(
+        this._createEndpointParam('baseImage', this._baseImage));
+    endpointDomApi.appendChild(
+        this._createEndpointParam('revisionImage', this._revisionImage));
+    td.appendChild(endpoint);
+    tr.appendChild(td);
+    tbody.appendChild(tr);
+    return tbody;
+  };
+
+  GrDiffBuilderImage.prototype._createEndpointParam = function(name, value) {
+    const endpointParam = this._createElement('gr-endpoint-param');
+    endpointParam.setAttribute('name', name);
+    endpointParam.value = value;
+    return endpointParam;
   };
 
   GrDiffBuilderImage.prototype._emitImagePair = function(section) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 9aec1e8..e8f4b21 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -179,7 +179,7 @@
           return this.$.processor.process(this.diff.content, isBinary)
               .then(() => {
                 if (this.isImageDiff) {
-                  this._builder.renderDiffImages();
+                  this._builder.renderDiff();
                 }
                 this.dispatchEvent(new CustomEvent('render-content',
                     {bubbles: true}));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 79b23ca..88ec720 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -32,7 +33,6 @@
 <link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
 <link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
 <link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html">
-
 <script src="../../../scripts/rootElement.js"></script>
 
 <dom-module id="gr-diff-comment">
@@ -387,6 +387,7 @@
     </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-diff-comment.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 8612011..b2b6b73 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -27,6 +27,10 @@
   const SAVING_PROGRESS_MESSAGE = 'Saving draft...';
   const DiSCARDING_PROGRESS_MESSAGE = 'Discarding draft...';
 
+  const REPORT_CREATE_DRAFT = 'CreateDraftComment';
+  const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
+  const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+
   Polymer({
     is: 'gr-diff-comment',
 
@@ -450,9 +454,12 @@
 
       // Ignore saves started while already saving.
       if (this.disabled) { return; }
-
+      const timingLabel = this.comment.id ?
+          REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
+      this.$.reporting.time(timingLabel);
       this.set('comment.__editing', false);
-      this.save();
+      return this.save()
+          .then(() => { this.$.reporting.timeEnd(timingLabel); });
     },
 
     _handleCancel(e) {
@@ -485,8 +492,10 @@
 
     _handleConfirmDiscard(e) {
       e.preventDefault();
+      this.$.reporting.time(REPORT_DISCARD_DRAFT);
       this._closeConfirmDiscardOverlay();
-      this._discardDraft();
+      return this._discardDraft()
+          .then(() => { this.$.reporting.timeEnd(REPORT_DISCARD_DRAFT); });
     },
 
     _discardDraft() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 5b43430..c7ca782 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -279,6 +279,45 @@
         });
       });
     });
+
+    suite('draft update reporting', () => {
+      let timeEndStub;
+      let mockEvent;
+
+      setup(() => {
+        mockEvent = {preventDefault() {}};
+        sandbox.stub(element, 'save')
+            .returns(Promise.resolve({}));
+        sandbox.stub(element, '_discardDraft')
+            .returns(Promise.resolve({}));
+        timeEndStub = sandbox.stub(element.$.reporting, 'timeEnd');
+      });
+
+      test('create', () => {
+        element.comment = {};
+        return element._handleSave(mockEvent).then(() => {
+          assert.isTrue(timeEndStub.calledOnce);
+          assert.equal(timeEndStub.lastCall.args[0], 'CreateDraftComment');
+        });
+      });
+
+      test('update', () => {
+        element.comment = {id: 'abc_123'};
+        return element._handleSave(mockEvent).then(() => {
+          assert.isTrue(timeEndStub.calledOnce);
+          assert.equal(timeEndStub.lastCall.args[0], 'UpdateDraftComment');
+        });
+      });
+
+      test('discard', () => {
+        element.comment = {id: 'abc_123'};
+        sandbox.stub(element, '_closeConfirmDiscardOverlay');
+        return element._handleConfirmDiscard(mockEvent).then(() => {
+          assert.isTrue(timeEndStub.calledOnce);
+          assert.equal(timeEndStub.lastCall.args[0], 'DiscardDraftComment');
+        });
+      });
+    });
   });
 
   suite('gr-diff-comment draft tests', () => {
@@ -578,6 +617,7 @@
         assert.isTrue(stub.called);
         stub.restore();
         done();
+        return Promise.resolve();
       });
       element._messageText = 'is that the horse from horsing around??';
       element.editing = true;
@@ -654,7 +694,7 @@
     });
 
     test('draft prevent save when disabled', () => {
-      const saveStub = sandbox.stub(element, 'save');
+      const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
       element.showActions = true;
       element.draft = true;
       MockInteractions.tap(element.$.header);
@@ -751,7 +791,7 @@
       });
 
       test('saving', () => {
-        element._saveDraft();
+        element._saveDraft({});
         assert.equal(element._savingMessage, 'Saving draft...');
       });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 81c6d99..540df98 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -108,7 +108,6 @@
         cursor: pointer;
       }
       .content {
-        overflow: hidden;
         /* Set min width since setting width on table cells still
            allows them to shrink. Do not set max width because
            CJK (Chinese-Japanese-Korean) glyphs have variable width */
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
index cd9f9dc..017cd5d 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-syntax-lib-loader/gr-syntax-lib-loader.html">
+<link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html">
 
 <dom-module id="gr-syntax-layer">
   <template>
-    <gr-syntax-lib-loader id="libLoader"></gr-syntax-lib-loader>
+    <gr-lib-loader id="libLoader"></gr-lib-loader>
   </template>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index f8db343..e258520 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -38,7 +38,6 @@
     'text/x-java': 'java',
     'text/x-kotlin': 'kotlin',
     'text/x-lua': 'lua',
-    'text/x-markdown': 'markdown',
     'text/x-objectivec': 'objectivec',
     'text/x-ocaml': 'ocaml',
     'text/x-perl': 'perl',
@@ -62,7 +61,6 @@
     'gr-diff gr-syntax gr-syntax-attribute': true,
     'gr-diff gr-syntax gr-syntax-built_in': true,
     'gr-diff gr-syntax gr-syntax-comment': true,
-    'gr-diff gr-syntax gr-syntax-emphasis': true,
     'gr-diff gr-syntax gr-syntax-keyword': true,
     'gr-diff gr-syntax gr-syntax-link': true,
     'gr-diff gr-syntax gr-syntax-literal': true,
@@ -77,7 +75,6 @@
     'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
     'gr-diff gr-syntax gr-syntax-selector-tag': true,
     'gr-diff gr-syntax gr-syntax-string': true,
-    'gr-diff gr-syntax gr-syntax-strong': true,
     'gr-diff gr-syntax gr-syntax-tag': true,
     'gr-diff gr-syntax gr-syntax-template-tag': true,
     'gr-diff gr-syntax gr-syntax-template-variable': true,
@@ -442,7 +439,7 @@
     },
 
     _loadHLJS() {
-      return this.$.libLoader.get().then(hljs => {
+      return this.$.libLoader.getHLJS().then(hljs => {
         this._hljs = hljs;
       });
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 74fc3bf..f2458fc 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -185,7 +185,7 @@
 
       const mockHLJS = getMockHLJS();
       const highlightSpy = sinon.spy(mockHLJS, 'highlight');
-      sandbox.stub(element.$.libLoader, 'get',
+      sandbox.stub(element.$.libLoader, 'getHLJS',
           () => { return Promise.resolve(mockHLJS); });
       const processNextSpy = sandbox.spy(element, '_processNextLine');
       const processPromise = element.process();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
deleted file mode 100644
index 6ec7ab2..0000000
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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.
- */
-(function() {
-  'use strict';
-
-  const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
-  const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/;
-
-  Polymer({
-    is: 'gr-syntax-lib-loader',
-
-    properties: {
-      _state: {
-        type: Object,
-
-        // NOTE: intended singleton.
-        value: {
-          configured: false,
-          loading: false,
-          callbacks: [],
-        },
-      },
-    },
-
-    get() {
-      return new Promise((resolve, reject) => {
-        // If the lib is totally loaded, resolve immediately.
-        if (this._getHighlightLib()) {
-          resolve(this._getHighlightLib());
-          return;
-        }
-
-        // If the library is not currently being loaded, then start loading it.
-        if (!this._state.loading) {
-          this._state.loading = true;
-          this._loadHLJS().then(this._onLibLoaded.bind(this)).catch(reject);
-        }
-
-        this._state.callbacks.push(resolve);
-      });
-    },
-
-    _onLibLoaded() {
-      const lib = this._getHighlightLib();
-      this._state.loading = false;
-      for (const cb of this._state.callbacks) {
-        cb(lib);
-      }
-      this._state.callbacks = [];
-    },
-
-    _getHighlightLib() {
-      const lib = window.hljs;
-      if (lib && !this._state.configured) {
-        this._state.configured = true;
-
-        lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
-      }
-      return lib;
-    },
-
-    _getLibRoot() {
-      if (this._cachedLibRoot) { return this._cachedLibRoot; }
-
-      const appLink = document.head
-        .querySelector('link[rel=import][href$="gr-app.html"]');
-
-      if (!appLink) { return null; }
-
-      return this._cachedLibRoot = appLink
-          .href
-          .match(LIB_ROOT_PATTERN)[1];
-    },
-    _cachedLibRoot: null,
-
-    _loadHLJS() {
-      return new Promise((resolve, reject) => {
-        const script = document.createElement('script');
-        const src = this._getHLJSUrl();
-
-        if (!src) {
-          reject(new Error('Unable to load blank HLJS url.'));
-          return;
-        }
-
-        script.src = src;
-        script.onload = resolve;
-        script.onerror = reject;
-        Polymer.dom(document.head).appendChild(script);
-      });
-    },
-
-    _getHLJSUrl() {
-      const root = this._getLibRoot();
-      if (!root) { return null; }
-      return root + HLJS_PATH;
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
index 3dfbf34..0e028d8 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -93,12 +93,6 @@
       .gr-syntax-template-tag {
         color: var(--syntax-template-tag-color);
       }
-      .gr-syntax-emphasis {
-        font-style: italic;
-      }
-      .gr-syntax-strong {
-        font-weight: 700;
-      }
     </style>
   </template>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index de62646..efcefe2 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -19,6 +19,11 @@
   if (localStorage.getItem('USE_SHADOW_DOM') === 'true') {
     window.Polymer = {
       dom: 'shadow',
+      passiveTouchGestures: true,
+    };
+  } else if (!window.Polymer) {
+    window.Polymer = {
+      passiveTouchGestures: true,
     };
   }
 </script>
@@ -56,6 +61,7 @@
 <link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
 <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
 <link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
+<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html">
 <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <script src="../scripts/util.js"></script>
@@ -200,7 +206,7 @@
       </div>
       <div>
         <a class="feedback"
-            href="https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue"
+            href$="[[_feedbackUrl]]"
             rel="noopener" target="_blank">Send feedback</a>
         <template is="dom-if" if="[[_computeShowGwtUiLink(_serverConfig)]]">
           |
@@ -214,8 +220,9 @@
           view="[[params.view]]"
           on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
     </gr-overlay>
-    <gr-overlay id="registration" with-backdrop>
+    <gr-overlay id="registrationOverlay" with-backdrop>
       <gr-registration-dialog
+          id="registrationDialog"
           settings-url="[[_settingsUrl]]"
           on-account-detail-update="_handleAccountDetailUpdate"
           on-close="_handleRegistrationDialogClose">
@@ -229,6 +236,7 @@
     <gr-plugin-host id="plugins"
         config="[[_serverConfig]]">
     </gr-plugin-host>
+    <gr-lib-loader id="libLoader"></gr-lib-loader>
     <gr-external-style id="externalStyle" name="app-theme"></gr-external-style>
   </template>
   <script src="gr-app.js" crossorigin="anonymous"></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 921415f..7acb680 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -88,6 +88,11 @@
         computed: '_computePluginScreenName(params)',
       },
       _settingsUrl: String,
+      _feedbackUrl: {
+        type: String,
+        value: 'https://bugs.chromium.org/p/gerrit/issues/entry' +
+          '?template=PolyGerrit%20Issue',
+      },
     },
 
     listeners: {
@@ -125,10 +130,13 @@
       });
       this.$.restAPI.getVersion().then(version => {
         this._version = version;
+        this._logWelcome();
       });
 
       if (window.localStorage.getItem('dark-theme')) {
-        this.importHref('../styles/themes/dark-theme.html');
+        this.$.libLoader.getDarkTheme().then(module => {
+          Polymer.dom(this.root).appendChild(module);
+        });
       }
 
       // Note: this is evaluated here to ensure that it only happens after the
@@ -189,13 +197,17 @@
         this.async(() => this.set('_showPluginScreen', true), 1);
       }
       if (this.params.justRegistered) {
-        this.$.registration.open();
+        this.$.registrationOverlay.open();
+        this.$.registrationDialog.loadData().then(() => {
+          this.$.registrationOverlay.refit();
+        });
       }
       this.$.header.unfloat();
     },
 
     _computeShowGwtUiLink(config) {
-      return config.gerrit.web_uis && config.gerrit.web_uis.includes('GWT');
+      return !window.DEPRECATE_GWT_UI &&
+          config.gerrit.web_uis && config.gerrit.web_uis.includes('GWT');
     },
 
     _handlePageError(e) {
@@ -269,7 +281,7 @@
 
     _handleRegistrationDialogClose(e) {
       this.params.justRegistered = false;
-      this.$.registration.close();
+      this.$.registrationOverlay.close();
     },
 
     _computeShadowClass(isShadowDom) {
@@ -307,5 +319,18 @@
     _computePluginScreenName({plugin, screen}) {
       return Gerrit._getPluginScreenName(plugin, screen);
     },
+
+    _logWelcome() {
+      console.group('Runtime Info');
+      console.log('Gerrit UI (PolyGerrit)');
+      console.log(`Gerrit Server Version: ${this._version}`);
+      if (window.VERSION_INFO) {
+        console.log(`UI Version Info: ${window.VERSION_INFO}`);
+      }
+      const renderTime = new Date(window.performance.timing.loadEventStart);
+      console.log(`Document loaded at: ${renderTime}`);
+      console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+      console.groupEnd();
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index 79c8a3b..7a8cd6f 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -32,6 +32,16 @@
       main {
         max-width: 46em;
       }
+      :host(.loading) main {
+        display: none;
+      }
+      .loadingMessage {
+        display: none;
+        font-style: italic;
+      }
+      :host(.loading) .loadingMessage {
+        display: block;
+      }
       hr {
         margin-top: 1em;
         margin-bottom: 1em;
@@ -54,9 +64,13 @@
       input {
         width: 20em;
       }
+      section.hide {
+        display: none;
+      }
     </style>
     <div class="container gr-form-styles">
       <header>Please confirm your contact information</header>
+      <div class="loadingMessage">Loading...</div>
       <main>
         <p>
           The following contact information was automatically obtained when you
@@ -73,7 +87,7 @@
               bind-value="{{_account.name}}"
               disabled="[[_saving]]">
         </section>
-        <section>
+        <section class$="[[_computeUsernameClass(_usernameMutable)]]">
           <div class="title">Username</div>
           <input
               is="iron-input"
@@ -108,7 +122,7 @@
             id="saveButton"
             primary
             link
-            disabled="[[_computeSaveDisabled(_account.name, _account.username, _account.email, _saving)]]"
+            disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]"
             on-tap="_handleSave">Save</gr-button>
       </footer>
     </div>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 051668c..c6cd578 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -43,32 +43,56 @@
           return {email: null, name: null, username: null};
         },
       },
+      _usernameMutable: {
+        type: Boolean,
+        computed: '_computeUsernameMutable(_serverConfig, _account.username)',
+      },
+      _loading: {
+        type: Boolean,
+        value: true,
+        observer: '_loadingChanged',
+      },
       _saving: {
         type: Boolean,
         value: false,
       },
+      _serverConfig: Object,
     },
 
     hostAttributes: {
       role: 'dialog',
     },
 
-    attached() {
-      this.$.restAPI.getAccount().then(account => {
+    loadData() {
+      this._loading = true;
+
+      const loadAccount = this.$.restAPI.getAccount().then(account => {
         // Using Object.assign here allows preservation of the default values
         // supplied in the value generating function of this._account, unless
         // they are overridden by properties in the account from the response.
         this._account = Object.assign({}, this._account, account);
       });
+
+      const loadConfig = this.$.restAPI.getConfig().then(config => {
+        this._serverConfig = config;
+      });
+
+      return Promise.all([loadAccount, loadConfig]).then(() => {
+        this._loading = false;
+      });
     },
 
     _save() {
       this._saving = true;
       const promises = [
         this.$.restAPI.setAccountName(this.$.name.value),
-        this.$.restAPI.setAccountUsername(this.$.username.value),
         this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''),
       ];
+
+      if (this._usernameMutable) {
+        promises.push(this.$.restAPI.setAccountUsername(this.$.username.value));
+      }
+
       return Promise.all(promises).then(() => {
         this._saving = false;
         this.fire('account-detail-update');
@@ -90,8 +114,21 @@
       this.fire('close');
     },
 
-    _computeSaveDisabled(name, username, email, saving) {
-      return !name || !username || !email || saving;
+    _computeSaveDisabled(name, email, saving) {
+      return !name || !email || saving;
+    },
+
+    _computeUsernameMutable(config, username) {
+      return config.auth.editable_account_fields.includes('USER_NAME') &&
+          !username;
+    },
+
+    _computeUsernameClass(usernameMutable) {
+      return usernameMutable ? '' : 'hide';
+    },
+
+    _loadingChanged() {
+      this.classList.toggle('loading', this._loading);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index e4560ff..93a3188 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -45,13 +45,13 @@
     let sandbox;
     let _listeners;
 
-    setup(done => {
+    setup(() => {
       sandbox = sinon.sandbox.create();
       _listeners = {};
 
       account = {
         name: 'name',
-        username: 'username',
+        username: null,
         email: 'email',
         secondary_emails: [
           'email2',
@@ -61,8 +61,6 @@
 
       stub('gr-rest-api-interface', {
         getAccount() {
-        // Once the account is resolved, we can let the test proceed.
-          flush(done);
           return Promise.resolve(account);
         },
         setAccountName(name) {
@@ -77,9 +75,15 @@
           account.email = email;
           return Promise.resolve();
         },
+        getConfig() {
+          return Promise.resolve(
+              {auth: {editable_account_fields: ['USER_NAME']}});
+        },
       });
 
       element = fixture('basic');
+
+      return element.loadData();
     });
 
     teardown(() => {
@@ -136,7 +140,7 @@
 
         // Nothing should be committed yet.
         assert.equal(account.name, 'name');
-        assert.equal(account.username, 'username');
+        assert.isNotOk(account.username);
         assert.equal(account.email, 'email');
 
         // Save and verify new values are committed.
@@ -158,12 +162,22 @@
 
     test('save btn disabled', () => {
       const compute = element._computeSaveDisabled;
-      assert.isTrue(compute('', '', '', false));
-      assert.isTrue(compute('', 'test', 'test', false));
-      assert.isTrue(compute('test', '', 'test', false));
-      assert.isTrue(compute('test', 'test', '', false));
-      assert.isTrue(compute('test', 'test', 'test', true));
-      assert.isFalse(compute('test', 'test', 'test', false));
+      assert.isTrue(compute('', '', false));
+      assert.isTrue(compute('', 'test', false));
+      assert.isTrue(compute('test', '', false));
+      assert.isTrue(compute('test', 'test', true));
+      assert.isFalse(compute('test', 'test', false));
+    });
+
+    test('_computeUsernameMutable', () => {
+      assert.isTrue(element._computeUsernameMutable(
+          {auth: {editable_account_fields: ['USER_NAME']}}, null));
+      assert.isFalse(element._computeUsernameMutable(
+          {auth: {editable_account_fields: ['USER_NAME']}}, 'abc'));
+      assert.isFalse(element._computeUsernameMutable(
+          {auth: {editable_account_fields: []}}, null));
+      assert.isFalse(element._computeUsernameMutable(
+          {auth: {editable_account_fields: []}}, 'abc'));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
index fc029ce..bc63acf 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -16,9 +16,10 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-avatar">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index f559148..f32e940b 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -29,37 +29,40 @@
         type: Number,
         value: 16,
       },
+      _hasAvatars: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     behaviors: [
       Gerrit.BaseUrlBehavior,
     ],
 
-    created() {
-      this.hidden = true;
-    },
-
     attached() {
-      this.$.restAPI.getConfig().then(cfg => {
-        const hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
-        if (hasAvatars) {
-          this.hidden = false;
+      Promise.all([
+        this.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(([cfg]) => {
+        this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+        if (this._hasAvatars && this.account) {
           // src needs to be set if avatar becomes visible
-          this._updateAvatarURL(this.account);
+          this._updateAvatarURL();
+        } else {
+          this.hidden = true;
         }
       });
     },
 
     _accountChanged(account) {
-      this._updateAvatarURL(account);
+      this._updateAvatarURL();
     },
 
-    _updateAvatarURL(account) {
-      if (!this.hidden && account) {
-        const url = this._buildAvatarURL(this.account);
-        if (url) {
-          this.style.backgroundImage = 'url("' + url + '")';
-        }
+    _updateAvatarURL() {
+      if (this.hidden || !this._hasAvatars) { return; }
+      const url = this._buildAvatarURL(this.account);
+      if (url) {
+        this.style.backgroundImage = 'url("' + url + '")';
       }
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 333f0e8..f137c7f 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -38,7 +38,7 @@
 
     setup(() => {
       stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({}); },
+        getConfig() { return Promise.resolve({plugin: {has_avatars: true}}); },
       });
       element = fixture('basic');
     });
@@ -97,23 +97,36 @@
     });
 
     test('dom for existing account', () => {
-      assert.isTrue(element.hasAttribute('hidden'),
-          'element not hidden initially');
-      element.hidden = false;
+      assert.isFalse(element.hasAttribute('hidden'));
       element.imageSize = 64;
       element.account = {
         _account_id: 123,
       };
-      assert.isFalse(element.hasAttribute('hidden'), 'element hidden');
-      assert.isTrue(
-          element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
+      assert.strictEqual(element.style.backgroundImage, '');
+      // Emulate plugins loaded.
+      Gerrit._setPluginsPending([]);
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isFalse(element.hasAttribute('hidden'));
+        assert.isTrue(
+            element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
+      });
     });
 
     test('dom for non available account', () => {
-      assert.isTrue(element.hasAttribute('hidden'),
-          'element not hidden initially');
-      element.account = undefined;
-      assert.isTrue(element.hasAttribute('hidden'), 'element not hidden');
+      assert.isFalse(element.hasAttribute('hidden'));
+      element.account = null;
+      assert.isFalse(element.hasAttribute('hidden'));
+      // Emulate plugins loaded.
+      Gerrit._setPluginsPending([]);
+      return Promise.all([
+        element.$.restAPI.getConfig(),
+        Gerrit.awaitPluginsLoaded(),
+      ]).then(() => {
+        assert.isTrue(element.hasAttribute('hidden'));
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
new file mode 100644
index 0000000..7e3246f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
@@ -0,0 +1,49 @@
+<!--
+@license
+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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-hovercard">
+  <template>
+    <style include="shared-styles">
+      :host {
+        box-sizing: border-box;
+        opacity: 0;
+        position: absolute;
+        transition: opacity 200ms;
+        visibility: hidden;
+        z-index: 100;
+      }
+      :host(.hovered) {
+        visibility: visible;
+        opacity: 1;
+      }
+      :host ::content #hovercard {
+        background: var(--dialog-background-color);
+        box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
+        padding: 1em;
+      }
+    </style>
+    <div id="hovercard" role="tooltip" tabindex="-1">
+      <slot></slot>
+    </div>
+  </template>
+  <script src="../../../scripts/rootElement.js"></script>
+  <script src="gr-hovercard.js"></script>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
new file mode 100644
index 0000000..f9c1da1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -0,0 +1,321 @@
+/**
+ * @license
+ * 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.
+ */
+(function() {
+  'use strict';
+  const HOVER_CLASS = 'hovered';
+
+  /**
+   * When the hovercard is positioned diagonally (bottom-left, bottom-right,
+   * top-left, or top-right), we add additional (invisible) padding so that the
+   * area that a user can hover over to access the hovercard is larger.
+   */
+  const DIAGONAL_OVERFLOW = 15;
+
+  Polymer({
+    is: 'gr-hovercard',
+
+    properties: {
+      /**
+       * @type {?}
+       */
+      _target: Object,
+
+      /**
+       * Determines whether or not the hovercard is visible.
+       *
+       * @type {boolean}
+       */
+      _isShowing: {
+        type: Boolean,
+        value: false,
+      },
+      /**
+       * The `id` of the element that the hovercard is anchored to.
+       *
+       * @type {string}
+       */
+      for: {
+        type: String,
+        observer: '_forChanged',
+      },
+
+      /**
+       * The spacing between the top of the hovercard and the element it is
+       * anchored to.
+       *
+       * @type {number}
+       */
+      offset: {
+        type: Number,
+        value: 14,
+      },
+
+      /**
+       * Positions the hovercard to the top, right, bottom, left, bottom-left,
+       * bottom-right, top-left, or top-right of its content.
+       *
+       * @type {string}
+       */
+      position: {
+        type: String,
+        value: 'bottom',
+      },
+
+      container: Object,
+      /**
+       * ID for the container element.
+       *
+       * @type {string}
+       */
+      containerId: {
+        type: String,
+        value: 'gr-hovercard-container',
+      },
+    },
+
+    listeners: {
+      mouseleave: 'hide',
+    },
+
+    attached() {
+      if (!this._target) { this._target = this.target; }
+      this.listen(this._target, 'mouseenter', 'show');
+      this.listen(this._target, 'focus', 'show');
+      this.listen(this._target, 'mouseleave', 'hide');
+      this.listen(this._target, 'blur', 'hide');
+      this.listen(this._target, 'tap', 'hide');
+    },
+
+    ready() {
+      // First, check to see if the container has already been created.
+      this.container = Gerrit.getRootElement()
+          .querySelector('#' + this.containerId);
+
+      if (this.container) { return; }
+
+      // If it does not exist, create and initialize the hovercard container.
+      this.container = document.createElement('div');
+      this.container.setAttribute('id', this.containerId);
+      Gerrit.getRootElement().appendChild(this.container);
+    },
+
+    removeListeners() {
+      this.unlisten(this._target, 'mouseenter', 'show');
+      this.unlisten(this._target, 'focus', 'show');
+      this.unlisten(this._target, 'mouseleave', 'hide');
+      this.unlisten(this._target, 'blur', 'hide');
+      this.unlisten(this._target, 'tap', 'hide');
+    },
+
+    /**
+     * Returns the target element that the hovercard is anchored to (the `id` of
+     * the `for` property).
+     *
+     * @type {HTMLElement}
+     */
+    get target() {
+      const parentNode = Polymer.dom(this).parentNode;
+      // If the parentNode is a document fragment, then we need to use the host.
+      const ownerRoot = Polymer.dom(this).getOwnerRoot();
+      let target;
+      if (this.for) {
+        target = Polymer.dom(ownerRoot).querySelector('#' + this.for);
+      } else {
+        target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
+            ownerRoot.host :
+            parentNode;
+      }
+      return target;
+    },
+
+    /**
+     * Hides/closes the hovercard. This occurs when the user triggers the
+     * `mouseleave` event on the hovercard's `target` element (as long as the
+     * user is not hovering over the hovercard).
+     *
+     * @param {Event} e DOM Event (e.g. `mouseleave` event)
+     */
+    hide(e) {
+      const targetRect = this._target.getBoundingClientRect();
+      const x = e.clientX;
+      const y = e.clientY;
+      if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
+          y < targetRect.bottom) {
+        // Sometimes the hovercard itself obscures the mouse pointer, and
+        // that generates a mouseleave event. We don't want to hide the hovercard
+        // in that situation.
+        return;
+      }
+
+      // If the hovercard is already hidden or the user is now hovering over the
+      //  hovercard or the user is returning from the hovercard but now hovering
+      //  over the target (to stop an annoying flicker effect), just return.
+      if (!this._isShowing || e.toElement === this ||
+          (e.fromElement === this && e.toElement === this._target)) {
+        return;
+      }
+
+      // Mark that the hovercard is not visible and do not allow focusing
+      this._isShowing = false;
+
+      // Clear styles in preparation for the next time we need to show the card
+      this.classList.remove(HOVER_CLASS);
+
+      // Reset and remove the hovercard from the DOM
+      this.style.cssText = '';
+      this.$.hovercard.setAttribute('tabindex', -1);
+
+      // Remove the hovercard from the container, given that it is still a child
+      // of the container.
+      if (this.container.contains(this)) {
+        this.container.removeChild(this);
+      }
+    },
+
+    /**
+     * Shows/opens the hovercard. This occurs when the user triggers the
+     * `mousenter` event on the hovercard's `target` element.
+     *
+     * @param {Event} e DOM Event (e.g., `mouseenter` event)
+     */
+    show(e) {
+      if (this._isShowing) {
+        return;
+      }
+
+      // Mark that the hovercard is now visible
+      this._isShowing = true;
+      this.setAttribute('tabindex', 0);
+
+      // Add it to the DOM and calculate its position
+      this.container.appendChild(this);
+      this.updatePosition();
+
+      // Trigger the transition
+      this.classList.add(HOVER_CLASS);
+    },
+
+    /**
+     * Updates the hovercard's position based on the `position` attribute
+     * and the current position of the `target` element.
+     *
+     * The hovercard is supposed to stay open if the user hovers over it.
+     * To keep it open when the user moves away from the target, the bounding
+     * rects of the target and hovercard must touch or overlap.
+     *
+     * NOTE: You do not need to directly call this method unless you need to
+     * update the position of the tooltip while it is already visible (the
+     * target element has moved and the tooltip is still open).
+     */
+    updatePosition() {
+      if (!this._target) { return; }
+
+      // Calculate the necessary measurements and positions
+      const parentRect = document.documentElement.getBoundingClientRect();
+      const targetRect = this._target.getBoundingClientRect();
+      const thisRect = this.getBoundingClientRect();
+
+      const targetLeft = targetRect.left - parentRect.left;
+      const targetTop = targetRect.top - parentRect.top;
+
+      let hovercardLeft;
+      let hovercardTop;
+      const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
+      let cssText = '';
+
+      // Find the top and left position values based on the position attribute
+      // of the hovercard.
+      switch (this.position) {
+        case 'top':
+          hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+          hovercardTop = targetTop - thisRect.height - this.offset;
+          cssText += `padding-bottom:${this.offset
+              }px; margin-bottom:-${this.offset}px;`;
+          break;
+        case 'bottom':
+          hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+          hovercardTop = targetTop + targetRect.height + this.offset;
+          cssText +=
+              `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
+          break;
+        case 'left':
+          hovercardLeft = targetLeft - thisRect.width - this.offset;
+          hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+          cssText +=
+              `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
+          break;
+        case 'right':
+          hovercardLeft = targetRect.right + this.offset;
+          hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+          cssText +=
+              `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
+          break;
+        case 'bottom-right':
+          hovercardLeft = targetRect.left + targetRect.width + this.offset;
+          hovercardTop = targetRect.top + targetRect.height + this.offset;
+          cssText += `padding-top:${diagonalPadding}px;`;
+          cssText += `padding-left:${diagonalPadding}px;`;
+          cssText += `margin-left:-${diagonalPadding}px;`;
+          cssText += `margin-top:-${diagonalPadding}px;`;
+          break;
+        case 'bottom-left':
+          hovercardLeft = targetRect.left - thisRect.width - this.offset;
+          hovercardTop = targetRect.top + targetRect.height + this.offset;
+          cssText += `padding-top:${diagonalPadding}px;`;
+          cssText += `padding-right:${diagonalPadding}px;`;
+          cssText += `margin-right:-${diagonalPadding}px;`;
+          cssText += `margin-top:-${diagonalPadding}px;`;
+          break;
+        case 'top-left':
+          hovercardLeft = targetRect.left - thisRect.width - this.offset;
+          hovercardTop = targetRect.top - thisRect.height - this.offset;
+          cssText += `padding-bottom:${diagonalPadding}px;`;
+          cssText += `padding-right:${diagonalPadding}px;`;
+          cssText += `margin-bottom:-${diagonalPadding}px;`;
+          cssText += `margin-right:-${diagonalPadding}px;`;
+          break;
+        case 'top-right':
+          hovercardLeft = targetRect.left + targetRect.width + this.offset;
+          hovercardTop = targetRect.top - thisRect.height - this.offset;
+          cssText += `padding-bottom:${diagonalPadding}px;`;
+          cssText += `padding-left:${diagonalPadding}px;`;
+          cssText += `margin-bottom:-${diagonalPadding}px;`;
+          cssText += `margin-left:-${diagonalPadding}px;`;
+          break;
+      }
+
+      // Prevent hovercard from appearing outside the viewport.
+      // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
+      // right.
+      if (hovercardLeft < 0) { hovercardLeft = 0; }
+      if (hovercardTop < 0) { hovercardTop = 0; }
+      // Set the hovercard's position
+      cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
+      this.style.cssText = cssText;
+    },
+
+    /**
+     * Responds to a change in the `for` value and gets the updated `target`
+     * element for the hovercard.
+     *
+     * @private
+     */
+    _forChanged() {
+      this._target = this.target;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
new file mode 100644
index 0000000..e3e252f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+@license
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-hovercard</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+
+<link rel="import" href="gr-hovercard.html">
+
+<script>void(0);</script>
+
+<button id="foo">Hello</button>
+<test-fixture id="basic">
+  <template>
+    <gr-hovercard for="foo" id="bar"></gr-hovercard>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-hovercard tests', () => {
+    let element;
+    let sandbox;
+    const TRANSITION_TIME = 200;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('updatePosition', () => {
+      // Test that the correct style properties have at least been set.
+      element.position = 'bottom';
+      element.updatePosition();
+      assert.typeOf(element.style.getPropertyValue('left'), 'string');
+      assert.typeOf(element.style.getPropertyValue('top'), 'string');
+      assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
+      assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
+
+      const parentRect = document.documentElement.getBoundingClientRect();
+      const targetRect = element._target.getBoundingClientRect();
+      const thisRect = element.getBoundingClientRect();
+
+      const targetLeft = targetRect.left - parentRect.left;
+      const targetTop = targetRect.top - parentRect.top;
+
+      const pixelCompare = pixel =>
+        Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
+
+      assert.equal(
+          pixelCompare(element.style.left),
+          pixelCompare(
+              (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
+      assert.equal(
+          pixelCompare(element.style.top),
+          pixelCompare(
+              (targetTop + targetRect.height + element.offset) + 'px'));
+    });
+
+    test('hide', done => {
+      element.hide({});
+      setTimeout(() => {
+        const style = getComputedStyle(element);
+        assert.isFalse(element._isShowing);
+        assert.isFalse(element.classList.contains('hovered'));
+        assert.equal(style.opacity, '0');
+        assert.equal(style.visibility, 'hidden');
+        assert.notEqual(element.container, Polymer.dom(element).parentNode);
+        done();
+      }, TRANSITION_TIME);
+    });
+
+    test('show', done => {
+      element.show({});
+      setTimeout(() => {
+        const style = getComputedStyle(element);
+        assert.isTrue(element._isShowing);
+        assert.isTrue(element.classList.contains('hovered'));
+        assert.equal(style.opacity, '1');
+        assert.equal(style.visibility, 'visible');
+        done();
+      }, TRANSITION_TIME);
+    });
+
+    test('card shows on enter and hides on leave', done => {
+      const button = Polymer.dom(document).querySelector('button');
+      assert.isFalse(element._isShowing);
+      button.addEventListener('mouseenter', event => {
+        assert.isTrue(element._isShowing);
+        button.dispatchEvent(new CustomEvent('mouseleave'));
+      });
+      button.addEventListener('mouseleave', event => {
+        assert.isFalse(element._isShowing);
+        done();
+      });
+      button.dispatchEvent(new CustomEvent('mouseenter'));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index cbae987..9102bdb 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -40,6 +40,12 @@
       <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></g>
       <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
       <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></g>
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+      <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g>
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
+      <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"/></g>
       <!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full-->
       <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"/><path d="M0 0h24v24H0V0z" fill="none"/></g>
       <!-- This is a custom PolyGerrit SVG -->
@@ -51,8 +57,25 @@
       <!-- This is a custom PolyGerrit SVG -->
       <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g>
       <!-- This is a custom PolyGerrit SVG -->
-      <g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path>
-    </g>
+      <g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="abandon"><path d="M17.65675,17.65725 C14.77275,20.54125 10.23775,20.75625 7.09875,18.31525 L18.31475,7.09925 C20.75575,10.23825 20.54075,14.77325 17.65675,17.65725 M6.34275,6.34325 C9.22675,3.45925 13.76275,3.24425 16.90075,5.68525 L5.68475,16.90125 C3.24375,13.76325 3.45875,9.22725 6.34275,6.34325 M19.07075,4.92925 C15.16575,1.02425 8.83375,1.02425 4.92875,4.92925 C1.02375,8.83425 1.02375,15.16625 4.92875,19.07125 C8.83375,22.97625 15.16575,22.97625 19.07075,19.07125 C22.97575,15.16625 22.97575,8.83425 19.07075,4.92925"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="edit"><path d="M3,17.2525 L3,21.0025 L6.75,21.0025 L17.81,9.9425 L14.06,6.1925 L3,17.2525 L3,17.2525 Z M20.71,7.0425 C21.1,6.6525 21.1,6.0225 20.71,5.6325 L18.37,3.2925 C17.98,2.9025 17.35,2.9025 16.96,3.2925 L15.13,5.1225 L18.88,8.8725 L20.71,7.0425 L20.71,7.0425 Z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="rebase"><path d="M15.5759,19.4241 L14.5861,20.4146 L11.7574,23.2426 L10.3434,21.8286 L12.171569,20 L7.82933006,20 C7.41754308,21.1652555 6.30635522,22 5,22 C3.343,22 2,20.657 2,19 C2,17.6936448 2.83474451,16.5824569 4,16.1706699 L4,7.82933006 C2.83474451,7.41754308 2,6.30635522 2,5 C2,3.343 3.343,2 5,2 C6.30635522,2 7.41754308,2.83474451 7.82933006,4 L12.1715,4 L10.3431,2.1716 L11.7571,0.7576 L15.36365,4.3633 L16.0000001,4.99920039 C16.0004321,3.34256796 17.3432665,2 19,2 C20.657,2 22,3.343 22,5 C22,6.30635522 21.1652555,7.41754308 20,7.82933006 L20,16.1706699 C21.1652555,16.5824569 22,17.6936448 22,19 C22,20.657 20.657,22 19,22 C17.343,22 16,20.657 16,19 L15.5759,19.4241 Z M12.1715,18 L10.3431,16.1716 L11.7571,14.7576 L15.36365,18.3633 L16.0000001,18.9992004 C16.0003407,17.6931914 16.8349823,16.5823729 18,16.1706699 L18,7.82933006 C16.8347445,7.41754308 16,6.30635522 16,5 L15.5759,5.4241 L14.5861,6.4146 L11.7574,9.2426 L10.3434,7.8286 L12.171569,6 L7.82933006,6 C7.52807271,6.85248394 6.85248394,7.52807271 6,7.82933006 L6,16.1706699 C6.85248394,16.4719273 7.52807271,17.1475161 7.82933006,18 L12.1715,18 Z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="rebaseEdit"><path d="M15.5759,19.4241 L14.5861,20.4146 L11.7574,23.2426 L10.3434,21.8286 L12.171569,20 L7.82933006,20 C7.41754308,21.1652555 6.30635522,22 5,22 C3.343,22 2,20.657 2,19 C2,17.6936448 2.83474451,16.5824569 4,16.1706699 L4,7.82933006 C2.83474451,7.41754308 2,6.30635522 2,5 C2,3.343 3.343,2 5,2 C6.30635522,2 7.41754308,2.83474451 7.82933006,4 L12.1715,4 L10.3431,2.1716 L11.7571,0.7576 L15.36365,4.3633 L16.0000001,4.99920039 C16.0004321,3.34256796 17.3432665,2 19,2 C20.657,2 22,3.343 22,5 C22,6.30635522 21.1652555,7.41754308 20,7.82933006 L20,16.1706699 C21.1652555,16.5824569 22,17.6936448 22,19 C22,20.657 20.657,22 19,22 C17.343,22 16,20.657 16,19 L15.5759,19.4241 Z M12.1715,18 L10.3431,16.1716 L11.7571,14.7576 L15.36365,18.3633 L16.0000001,18.9992004 C16.0003407,17.6931914 16.8349823,16.5823729 18,16.1706699 L18,7.82933006 C16.8347445,7.41754308 16,6.30635522 16,5 L15.5759,5.4241 L14.5861,6.4146 L11.7574,9.2426 L10.3434,7.8286 L12.171569,6 L7.82933006,6 C7.52807271,6.85248394 6.85248394,7.52807271 6,7.82933006 L6,16.1706699 C6.85248394,16.4719273 7.52807271,17.1475161 7.82933006,18 L12.1715,18 Z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="restore"><path d="M12,8 L12,13 L16.28,15.54 L17,14.33 L13.5,12.25 L13.5,8 L12,8 Z M13,3 C8.03,3 4,7.03 4,12 L1,12 L4.89,15.89 L4.96,16.03 L9,12 L6,12 C6,8.13 9.13,5 13,5 C16.87,5 20,8.13 20,12 C20,15.87 16.87,19 13,19 C11.07,19 9.32,18.21 8.06,16.94 L6.64,18.36 C8.27,19.99 10.51,21 13,21 C17.97,21 22,16.97 22,12 C22,7.03 17.97,3 13,3 Z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="revert"><path d="M12.3,8.5 C9.64999995,8.5 7.24999995,9.49 5.39999995,11.1 L1.79999995,7.5 L1.79999995,16.5 L10.8,16.5 L7.17999995,12.88 C8.56999995,11.72 10.34,11 12.3,11 C15.84,11 18.85,13.31 19.9,16.5 L22.27,15.72 C20.88,11.53 16.95,8.5 12.3,8.5"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="stopEdit"><path d="M4 4 20 4 20 20 4 20z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="submit"><path d="M22.23,5 L11.65,15.58 L7.47000001,11.41 L6.06000001,12.82 L11.65,18.41 L23.649,6.41 L22.23,5 Z M16.58,5 L10.239,11.34 L11.65,12.75 L17.989,6.41 L16.58,5 Z M0.400000006,12.82 L5.99000001,18.41 L7.40000001,17 L1.82000001,11.41 L0.400000006,12.82 Z"></path></g>
+      <!-- This is a custom PolyGerrit SVG -->
+      <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g>
     </defs>
   </svg>
 </iron-iconset-svg>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index e0e3e9a..105e543 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -119,6 +119,11 @@
     this._el.setActionButtonProp(key, 'enabled', enabled);
   };
 
+  GrChangeActionsInterface.prototype.setIcon = function(key, icon) {
+    ensureEl(this);
+    this._el.setActionButtonProp(key, 'icon', icon);
+  };
+
   GrChangeActionsInterface.prototype.getActionDetails = function(action) {
     ensureEl(this);
     return this._el.getActionDetails(action) ||
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 747e6b5..988ed96 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -144,9 +144,12 @@
           assert.isNotOk(button.disabled);
           changeActions.setLabel(key, 'Yo');
           changeActions.setEnabled(key, false);
+          changeActions.setIcon(key, 'pupper');
           flush(() => {
             assert.equal(button.getAttribute('data-label'), 'Yo');
             assert.isTrue(button.disabled);
+            assert.equal(Polymer.dom(button).querySelector('iron-icon').icon,
+                'gr-icons:pupper');
             done();
           });
         });
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
similarity index 88%
rename from polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html
rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
index f5b71be..f70aff4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
@@ -16,6 +16,6 @@
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
-<dom-module id="gr-syntax-lib-loader">
-  <script src="gr-syntax-lib-loader.js"></script>
+<dom-module id="gr-lib-loader">
+  <script src="gr-lib-loader.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
new file mode 100644
index 0000000..ef8c112
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright (C) 2016 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.
+ */
+(function() {
+  'use strict';
+
+  const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
+  const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
+
+  Polymer({
+    is: 'gr-lib-loader',
+
+    properties: {
+      _hljsState: {
+        type: Object,
+
+        // NOTE: intended singleton.
+        value: {
+          configured: false,
+          loading: false,
+          callbacks: [],
+        },
+      },
+    },
+
+    /**
+     * Get the HLJS library. Returns a promise that resolves with a reference to
+     * the library after it's been loaded. The promise resolves immediately if
+     * it's already been loaded.
+     * @return {!Promise<Object>}
+     */
+    getHLJS() {
+      return new Promise((resolve, reject) => {
+        // If the lib is totally loaded, resolve immediately.
+        if (this._getHighlightLib()) {
+          resolve(this._getHighlightLib());
+          return;
+        }
+
+        // If the library is not currently being loaded, then start loading it.
+        if (!this._hljsState.loading) {
+          this._hljsState.loading = true;
+          this._loadScript(this._getHLJSUrl())
+              .then(this._onHLJSLibLoaded.bind(this)).catch(reject);
+        }
+
+        this._hljsState.callbacks.push(resolve);
+      });
+    },
+
+    /**
+     * Loads the dark theme document. Returns a promise that resolves with a
+     * custom-style DOM element.
+     * @return {!Promise<Element>}
+     */
+    getDarkTheme() {
+      return new Promise((resolve, reject) => {
+        this.importHref(this._getLibRoot() + DARK_THEME_PATH, () => {
+          const module = document.createElement('style', 'custom-style');
+          module.setAttribute('include', 'dark-theme');
+          resolve(module);
+        });
+      });
+    },
+
+    /**
+     * Execute callbacks awaiting the HLJS lib load.
+     */
+    _onHLJSLibLoaded() {
+      const lib = this._getHighlightLib();
+      this._hljsState.loading = false;
+      for (const cb of this._hljsState.callbacks) {
+        cb(lib);
+      }
+      this._hljsState.callbacks = [];
+    },
+
+    /**
+     * Get the HLJS library, assuming it has been loaded. Configure the library
+     * if it hasn't already been configured.
+     * @return {!Object}
+     */
+    _getHighlightLib() {
+      const lib = window.hljs;
+      if (lib && !this._hljsState.configured) {
+        this._hljsState.configured = true;
+
+        lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
+      }
+      return lib;
+    },
+
+    /**
+     * Get the resource path used to load the application. If the application
+     * was loaded through a CDN, then this will be the path to CDN resources.
+     * @return {string}
+     */
+    _getLibRoot() {
+      if (window.STATIC_RESOURCE_PATH) {
+        return window.STATIC_RESOURCE_PATH + '/';
+      }
+      return '/';
+    },
+
+    /**
+     * Load and execute a JS file from the lib root.
+     * @param {string} src The path to the JS file without the lib root.
+     * @return {Promise} a promise that resolves when the script's onload
+     *     executes.
+     */
+    _loadScript(src) {
+      return new Promise((resolve, reject) => {
+        const script = document.createElement('script');
+
+        if (!src) {
+          reject(new Error('Unable to load blank script url.'));
+          return;
+        }
+
+        script.src = src;
+        script.onload = resolve;
+        script.onerror = reject;
+        Polymer.dom(document.head).appendChild(script);
+      });
+    },
+
+    _getHLJSUrl() {
+      const root = this._getLibRoot();
+      if (!root) { return null; }
+      return root + HLJS_PATH;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
similarity index 77%
rename from polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
index a260a97..cf9a41c 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -17,64 +17,67 @@
 -->
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-syntax-lib-loader</title>
+<title>gr-lib-loader</title>
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-syntax-lib-loader.html">
+<link rel="import" href="gr-lib-loader.html">
 
 <script>void(0);</script>
 
 <test-fixture id="basic">
   <template>
-    <gr-syntax-lib-loader></gr-syntax-lib-loader>
+    <gr-lib-loader></gr-lib-loader>
   </template>
 </test-fixture>
 
 <script>
-  suite('gr-syntax-lib-loader tests', () => {
+  suite('gr-lib-loader tests', () => {
+    let sandbox;
     let element;
     let resolveLoad;
     let loadStub;
 
     setup(() => {
+      sandbox = sinon.sandbox.create();
       element = fixture('basic');
 
-      loadStub = sinon.stub(element, '_loadHLJS', () =>
+      loadStub = sandbox.stub(element, '_loadScript', () =>
         new Promise(resolve => resolveLoad = resolve)
       );
 
       // Assert preconditions:
-      assert.isFalse(element._state.loading);
+      assert.isFalse(element._hljsState.loading);
     });
 
     teardown(() => {
       if (window.hljs) {
         delete window.hljs;
       }
-      loadStub.restore();
+      sandbox.restore();
 
       // Because the element state is a singleton, clean it up.
-      element._state.configured = false;
-      element._state.loading = false;
-      element._state.callbacks = [];
+      element._hljsState.configured = false;
+      element._hljsState.loading = false;
+      element._hljsState.callbacks = [];
     });
 
     test('only load once', done => {
+      sandbox.stub(element, '_getHLJSUrl').returns('');
       const firstCallHandler = sinon.stub();
-      element.get().then(firstCallHandler);
+      element.getHLJS().then(firstCallHandler);
 
       // It should now be in the loading state.
       assert.isTrue(loadStub.called);
-      assert.isTrue(element._state.loading);
+      assert.isTrue(element._hljsState.loading);
       assert.isFalse(firstCallHandler.called);
 
       const secondCallHandler = sinon.stub();
-      element.get().then(secondCallHandler);
+      element.getHLJS().then(secondCallHandler);
 
       // No change in state.
-      assert.isTrue(element._state.loading);
+      assert.isTrue(element._hljsState.loading);
       assert.isFalse(firstCallHandler.called);
       assert.isFalse(secondCallHandler.called);
 
@@ -82,7 +85,7 @@
       resolveLoad();
       flush(() => {
         // The state should be loaded and both handlers called.
-        assert.isFalse(element._state.loading);
+        assert.isFalse(element._hljsState.loading);
         assert.isTrue(firstCallHandler.called);
         assert.isTrue(secondCallHandler.called);
         done();
@@ -105,7 +108,7 @@
 
       test('returns hljs', done => {
         const firstCallHandler = sinon.stub();
-        element.get().then(firstCallHandler);
+        element.getHLJS().then(firstCallHandler);
         flush(() => {
           assert.isTrue(firstCallHandler.called);
           assert.isTrue(firstCallHandler.calledWith(hljsStub));
@@ -114,7 +117,7 @@
       });
 
       test('configures hljs', done => {
-        element.get().then(() => {
+        element.getHLJS().then(() => {
           assert.isTrue(window.hljs.configure.calledOnce);
           done();
         });
@@ -123,15 +126,10 @@
 
     suite('_getHLJSUrl', () => {
       suite('checking _getLibRoot', () => {
-        let libRootStub;
         let root;
 
         setup(() => {
-          libRootStub = sinon.stub(element, '_getLibRoot', () => root);
-        });
-
-        teardown(() => {
-          libRootStub.restore();
+          sandbox.stub(element, '_getLibRoot', () => root);
         });
 
         test('with no root', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 2c45311..22e14e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -43,7 +43,7 @@
       '_contentOrConfigChanged(content, config)',
     ],
 
-    _contentChanged: function(content) {
+    _contentChanged(content) {
       // In the case where the config may not be set (perhaps due to the
       // request for it still being in flight), set the content anyway to
       // prevent waiting on the config to display the text.
@@ -51,22 +51,19 @@
       this.$.output.textContent = content;
     },
 
-    _contentOrConfigChanged: function(content, config) {
+    /**
+     * Because either the source text or the linkification config has changed,
+     * the content should be re-parsed.
+     * @param {string|null|undefined} content The raw, un-linkified source
+     *     string to parse.
+     * @param {Object|null|undefined} config The server config specifying
+     *     commentLink patterns
+     */
+    _contentOrConfigChanged(content, config) {
       var output = Polymer.dom(this.$.output);
       output.textContent = '';
-      var parser = new GrLinkTextParser(
-          config, function(text, href, fragment) {
-        if (href) {
-          var a = document.createElement('a');
-          a.href = href;
-          a.textContent = text;
-          a.target = '_blank';
-          a.rel = 'noopener';
-          output.appendChild(a);
-        } else if (fragment) {
-          output.appendChild(fragment);
-        }
-      }, this.removeZeroWidthSpace);
+      var parser = new GrLinkTextParser(config,
+          this._handleParseResult.bind(this), this.removeZeroWidthSpace);
       parser.parse(content);
 
       // Ensure that links originating from HTML commentlink configs open in a
@@ -76,5 +73,31 @@
         anchor.setAttribute('rel', 'noopener');
       });
     },
+
+    /**
+     * This method is called when the GrLikTextParser emits a partial result
+     * (used as the "callback" parameter). It will be called in either of two
+     * ways:
+     * - To create a link: when called with `text` and `href` arguments, a link
+     *   element should be created and attached to the resulting DOM.
+     * - To attach an arbitrary fragment: when called with only the `fragment`
+     *   argument, the fragment should be attached to the resulting DOM as is.
+     * @param {string|null} text
+     * @param {string|null} href
+     * @param  {DocumentFragment|undefined} fragment
+     */
+    _handleParseResult(text, href, fragment) {
+      var output = Polymer.dom(this.$.output);
+      if (href) {
+        var a = document.createElement('a');
+        a.href = href;
+        a.textContent = text;
+        a.target = '_blank';
+        a.rel = 'noopener';
+        output.appendChild(a);
+      } else if (fragment) {
+        output.appendChild(fragment);
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 5978e37..baa025e 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -201,6 +201,31 @@
       assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
     });
 
+    test('only {http,https,mailto} protocols are linkified', function() {
+      element.content = 'xx mailto:test@google.com yy';
+      let links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+
+      element.content = 'xx http://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'http://google.com');
+
+      element.content = 'xx https://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'https://google.com');
+
+      element.content = 'xx ssh://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 0);
+
+      element.content = 'xx ftp://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 0);
+    });
+
     test('overlapping links', function() {
       element.config = {
         b1: {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index a84411f..8b49ca0 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -14,198 +14,315 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+(function() {
+  'use strict';
 
-'use strict';
+  const Defs = {};
 
-function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) {
-  this.linkConfig = linkConfig;
-  this.callback = callback;
-  this.removeZeroWidthSpace = opt_removeZeroWidthSpace;
-  Object.preventExtensions(this);
-}
+  /**
+   * @typedef {{
+   *    html: Node,
+   *    position: number,
+   *    length: number,
+   * }}
+   */
+  Defs.CommentLinkItem;
 
-GrLinkTextParser.prototype.addText = function(text, href) {
-  if (!text) {
-    return;
-  }
-  this.callback(text, href);
-};
+  /**
+   * Pattern describing URLs with supported protocols.
+   * @type {RegExp}
+   */
+  const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/;
 
-GrLinkTextParser.prototype.processLinks = function(text, outputArray) {
-  this.sortArrayReverse(outputArray);
-  var fragment = document.createDocumentFragment();
-  var cursor = text.length;
-
-  // Start inserting linkified URLs from the end of the String. That way, the
-  // string positions of the items don't change as we iterate through.
-  outputArray.forEach(function(item) {
-    // Add any text between the current linkified item and the item added before
-    // if it exists.
-    if (item.position + item.length !== cursor) {
-      fragment.insertBefore(
-          document.createTextNode(
-              text.slice(item.position + item.length, cursor)),
-          fragment.firstChild);
-    }
-    fragment.insertBefore(item.html, fragment.firstChild);
-    cursor = item.position;
-  });
-
-  // Add the beginning portion at the end.
-  if (cursor !== 0) {
-    fragment.insertBefore(
-        document.createTextNode(text.slice(0, cursor)), fragment.firstChild);
+  /**
+   * Construct a parser for linkifying text. Will linkify plain URLs that appear
+   * in the text as well as custom links if any are specified in the linkConfig
+   * parameter.
+   * @param {Object|null|undefined} linkConfig Comment links as specified by the
+   *     commentlinks field on a project config.
+   * @param {Function} callback The callback to be fired when an intermediate
+   *     parse result is emitted. The callback is passed text and href strings if
+   *     a link is to be created, or a document fragment otherwise.
+   * @param {boolean|undefined} opt_removeZeroWidthSpace If true, zero-width
+   *     spaces will be removed from R=<email> and CC=<email> expressions.
+   */
+  function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) {
+    this.linkConfig = linkConfig;
+    this.callback = callback;
+    this.removeZeroWidthSpace = opt_removeZeroWidthSpace;
+    Object.preventExtensions(this);
   }
 
-  this.callback(null, null, fragment);
-};
+  /**
+   * Emit a callback to create a link element.
+   * @param {string} text The text of the link.
+   * @param {string} href The URL to use as the href of the link.
+   */
+  GrLinkTextParser.prototype.addText = function(text, href) {
+    if (!text) { return; }
+    this.callback(text, href);
+  };
 
-GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) {
-  outputArray.sort(function(a, b) {return b.position - a.position});
-};
-
-GrLinkTextParser.prototype.addItem =
-    function(text, href, html, position, length, outputArray) {
-  var htmlOutput = '';
-
-  if (href) {
-    var a = document.createElement('a');
-    a.href = href;
-    a.textContent = text;
-    a.target = '_blank';
-    a.rel = 'noopener';
-    htmlOutput = a;
-  } else if (html) {
+  /**
+   * Given the source text and a list of CommentLinkItem objects that were
+   * generated by the commentlinks config, emit parsing callbacks.
+   * @param {string} text The chuml of source text over which the outputArray
+   *     items range.
+   * @param {!Array<Defs.CommentLinkItem>} outputArray The list of items to add
+   *     resulting from commentlink matches.
+   */
+  GrLinkTextParser.prototype.processLinks = function(text, outputArray) {
+    this.sortArrayReverse(outputArray);
     var fragment = document.createDocumentFragment();
-    // Create temporary div to hold the nodes in.
-    var div = document.createElement('div');
-    div.innerHTML = html;
-    while (div.firstChild) {
-      fragment.appendChild(div.firstChild);
+    var cursor = text.length;
+
+    // Start inserting linkified URLs from the end of the String. That way, the
+    // string positions of the items don't change as we iterate through.
+    outputArray.forEach(function(item) {
+      // Add any text between the current linkified item and the item added before
+      // if it exists.
+      if (item.position + item.length !== cursor) {
+        fragment.insertBefore(
+            document.createTextNode(
+                text.slice(item.position + item.length, cursor)),
+            fragment.firstChild);
+      }
+      fragment.insertBefore(item.html, fragment.firstChild);
+      cursor = item.position;
+    });
+
+    // Add the beginning portion at the end.
+    if (cursor !== 0) {
+      fragment.insertBefore(
+          document.createTextNode(text.slice(0, cursor)), fragment.firstChild);
     }
-    htmlOutput = fragment;
-  }
 
-  outputArray.push({
-    html: htmlOutput,
-    position: position,
-    length: length,
-  });
-};
+    this.callback(null, null, fragment);
+  };
 
-GrLinkTextParser.prototype.addLink =
-    function(text, href, position, length, outputArray) {
-  if (!text) {
-    return;
-  }
-  if (!this.hasOverlap(position, length, outputArray)) {
+  /**
+   * Sort the given array of CommentLinkItems such that the positions are in
+   * reverse order.
+   * @param {!Array<Defs.CommentLinkItem>} outputArray
+   */
+  GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) {
+    outputArray.sort((a, b) => b.position - a.position);
+  };
+
+  /**
+   * Create a CommentLinkItem and append it to the given output array. This
+   * method can be called in either of two ways:
+   * - With `text` and `href` parameters provided, and the `html` parameter
+   *   passed as `null`. In this case, the new CommentLinkItem will be a link
+   *   element with the given text and href value.
+   * - With the `html` paremeter provided, and the `text` and `href` parameters
+   *   passed as `null`. In this case, the string of HTML will be parsed and the
+   *   first resulting node will be used as the resulting content.
+   * @param {string|null} text The text to use if creating a link.
+   * @param {string|null} href The href to use as the URL if creating a link.
+   * @param {string|null} html The html to parse and use as the result.
+   * @param {number} position The position inside the source text where the item
+   *     starts.
+   * @param {number} length The number of characters in the source text
+   *     represented by the item.
+   * @param {!Array<Defs.CommentLinkItem>} outputArray The array to which the
+   *     new item is to be appended.
+   */
+  GrLinkTextParser.prototype.addItem =
+      function(text, href, html, position, length, outputArray) {
+    var htmlOutput = '';
+
+    if (href) {
+      var a = document.createElement('a');
+      a.href = href;
+      a.textContent = text;
+      a.target = '_blank';
+      a.rel = 'noopener';
+      htmlOutput = a;
+    } else if (html) {
+      var fragment = document.createDocumentFragment();
+      // Create temporary div to hold the nodes in.
+      var div = document.createElement('div');
+      div.innerHTML = html;
+      while (div.firstChild) {
+        fragment.appendChild(div.firstChild);
+      }
+      htmlOutput = fragment;
+    }
+
+    outputArray.push({
+      html: htmlOutput,
+      position: position,
+      length: length,
+    });
+  };
+
+  /**
+   * Create a CommentLinkItem for a link and append it to the given output
+   * array.
+   * @param {string|null} text The text for the link.
+   * @param {string|null} href The href to use as the URL of the link.
+   * @param {number} position The position inside the source text where the link
+   *     starts.
+   * @param {number} length The number of characters in the source text
+   *     represented by the link.
+   * @param {!Array<Defs.CommentLinkItem>} outputArray The array to which the
+   *     new item is to be appended.
+   */
+  GrLinkTextParser.prototype.addLink =
+      function(text, href, position, length, outputArray) {
+    if (!text || this.hasOverlap(position, length, outputArray)) { return; }
     this.addItem(text, href, null, position, length, outputArray);
-  }
-};
+  };
 
-GrLinkTextParser.prototype.addHTML =
-    function(html, position, length, outputArray) {
-  if (!this.hasOverlap(position, length, outputArray)) {
+  /**
+   * Create a CommentLinkItem specified by an HTMl string and append it to the
+   * given output array.
+   * @param {string|null} html The html to parse and use as the result.
+   * @param {number} position The position inside the source text where the item
+   *     starts.
+   * @param {number} length The number of characters in the source text
+   *     represented by the item.
+   * @param {!Array<Defs.CommentLinkItem>} outputArray The array to which the
+   *     new item is to be appended.
+   */
+  GrLinkTextParser.prototype.addHTML =
+      function(html, position, length, outputArray) {
+    if (this.hasOverlap(position, length, outputArray)) { return; }
     this.addItem(null, null, html, position, length, outputArray);
-  }
-};
+  };
 
-GrLinkTextParser.prototype.hasOverlap =
-    function(position, length, outputArray) {
-  var endPosition = position + length;
-  for (var i = 0; i < outputArray.length; i++) {
-    var arrayItemStart = outputArray[i].position;
-    var arrayItemEnd = outputArray[i].position + outputArray[i].length;
-    if ((position >= arrayItemStart && position < arrayItemEnd) ||
-      (endPosition > arrayItemStart && endPosition <= arrayItemEnd) ||
-      (position === arrayItemStart && position === arrayItemEnd)) {
-          return true;
-    }
-  }
-  return false;
-};
-
-GrLinkTextParser.prototype.parse = function(text) {
-  linkify(text, {
-    callback: this.parseChunk.bind(this),
-  });
-};
-
-GrLinkTextParser.prototype.parseChunk = function(text, href) {
-  // TODO(wyatta) switch linkify sequence, see issue 5526.
-  if (this.removeZeroWidthSpace) {
-    // Remove the zero-width space added in gr-change-view.
-    text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
-  }
-
-  if (href) {
-    this.addText(text, href);
-  } else {
-    this.parseLinks(text, this.linkConfig);
-  }
-};
-
-GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
-  // The outputArray is used to store all of the matches found for all patterns.
-  var outputArray = [];
-  for (var p in patterns) {
-    if (patterns[p].enabled != null && patterns[p].enabled == false) {
-      continue;
-    }
-    // PolyGerrit doesn't use hash-based navigation like GWT.
-    // Account for this.
-    // TODO(andybons): Support Gerrit being served from a base other than /,
-    // e.g. https://git.eclipse.org/r/
-    if (patterns[p].html) {
-      patterns[p].html =
-          patterns[p].html.replace(/<a href=\"#\//g, '<a href="/');
-    } else if (patterns[p].link) {
-      if (patterns[p].link[0] == '#') {
-        patterns[p].link = patterns[p].link.substr(1);
+  /**
+   * Does the given range overlap with anything already in the item list.
+   * @param {number} position
+   * @param {number} length
+   * @param {!Array<Defs.CommentLinkItem>} outputArray
+   */
+  GrLinkTextParser.prototype.hasOverlap =
+      function(position, length, outputArray) {
+    var endPosition = position + length;
+    for (var i = 0; i < outputArray.length; i++) {
+      var arrayItemStart = outputArray[i].position;
+      var arrayItemEnd = outputArray[i].position + outputArray[i].length;
+      if ((position >= arrayItemStart && position < arrayItemEnd) ||
+        (endPosition > arrayItemStart && endPosition <= arrayItemEnd) ||
+        (position === arrayItemStart && position === arrayItemEnd)) {
+            return true;
       }
     }
+    return false;
+  };
 
-    var pattern = new RegExp(patterns[p].match, 'g');
+  /**
+   * Parse the given source text and emit callbacks for the items that are
+   * parsed.
+   * @param {string} text
+   */
+  GrLinkTextParser.prototype.parse = function(text) {
+    linkify(text, {
+      callback: this.parseChunk.bind(this),
+    });
+  };
 
-    var match;
-    var textToCheck = text;
-    var susbtrIndex = 0;
+  /**
+   * Callback that is pased into the linkify function. ba-linkify will call this
+   * method in either of two ways:
+   * - With both a `text` and `href` parameter provided: this indicates that
+   *   ba-linkify has found a plain URL and wants it linkified.
+   * - With only a `text` parameter provided: this represents the non-link
+   *   content that lies between the links the library has found.
+   * @param {string} text
+   * @param {string|null|undefined} href
+   */
+  GrLinkTextParser.prototype.parseChunk = function(text, href) {
+    // TODO(wyatta) switch linkify sequence, see issue 5526.
+    if (this.removeZeroWidthSpace) {
+      // Remove the zero-width space added in gr-change-view.
+      text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
+    }
 
-    while ((match = pattern.exec(textToCheck)) != null) {
-      textToCheck = textToCheck.substr(match.index + match[0].length);
-      var result = match[0].replace(pattern,
-          patterns[p].html || patterns[p].link);
+    // If the href is provided then ba-linkify has recognized it as a URL. If the
+    // source text does not include a protocol, the protocol will be added by
+    // ba-linkify. Create the link if the href is provided and its protocol
+    // matches the expected pattern.
+    if (href && URL_PROTOCOL_PATTERN.test(href)) {
+      this.addText(text, href);
+    } else {
+      // For the sections of text that lie between the links found by
+      // ba-linkify, we search for the project-config-specified link patterns.
+      this.parseLinks(text, this.linkConfig);
+    }
+  };
 
-      // Skip portion of replacement string that is equal to original.
-      for (var i = 0; i < result.length; i++) {
-        if (result[i] !== match[0][i]) {
-          break;
+  /**
+   * Walk over the given source text to find matches for comemntlink patterns
+   * and emit parse result callbacks.
+   * @param {string} text The raw source text.
+   * @param {Object|null|undefined} patterns A comment links specification
+   *   object.
+   */
+  GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
+    // The outputArray is used to store all of the matches found for all patterns.
+    var outputArray = [];
+    for (var p in patterns) {
+      if (patterns[p].enabled != null && patterns[p].enabled == false) {
+        continue;
+      }
+      // PolyGerrit doesn't use hash-based navigation like the GWT UI.
+      // Account for this.
+      if (patterns[p].html) {
+        patterns[p].html =
+            patterns[p].html.replace(/<a href=\"#\//g, '<a href="/');
+      } else if (patterns[p].link) {
+        if (patterns[p].link[0] == '#') {
+          patterns[p].link = patterns[p].link.substr(1);
         }
       }
-      result = result.slice(i);
 
-      if (patterns[p].html) {
-        this.addHTML(
-          result,
-          susbtrIndex + match.index + i,
-          match[0].length - i,
-          outputArray);
-      } else if (patterns[p].link) {
-        this.addLink(
-          match[0],
-          result,
-          susbtrIndex + match.index + i,
-          match[0].length - i,
-          outputArray);
-      } else {
-        throw Error('linkconfig entry ' + p +
-            ' doesn’t contain a link or html attribute.');
+      var pattern = new RegExp(patterns[p].match, 'g');
+
+      var match;
+      var textToCheck = text;
+      var susbtrIndex = 0;
+
+      while ((match = pattern.exec(textToCheck)) != null) {
+        textToCheck = textToCheck.substr(match.index + match[0].length);
+        var result = match[0].replace(pattern,
+            patterns[p].html || patterns[p].link);
+
+        // Skip portion of replacement string that is equal to original.
+        for (var i = 0; i < result.length; i++) {
+          if (result[i] !== match[0][i]) {
+            break;
+          }
+        }
+        result = result.slice(i);
+
+        if (patterns[p].html) {
+          this.addHTML(
+            result,
+            susbtrIndex + match.index + i,
+            match[0].length - i,
+            outputArray);
+        } else if (patterns[p].link) {
+          this.addLink(
+            match[0],
+            result,
+            susbtrIndex + match.index + i,
+            match[0].length - i,
+            outputArray);
+        } else {
+          throw Error('linkconfig entry ' + p +
+              ' doesn’t contain a link or html attribute.');
+        }
+
+        // Update the substring location so we know where we are in relation to
+        // the initial full text string.
+        susbtrIndex = susbtrIndex + match.index + match[0].length;
       }
-
-      // Update the substring location so we know where we are in relation to
-      // the initial full text string.
-      susbtrIndex = susbtrIndex + match.index + match[0].length;
     }
-  }
-  this.processLinks(text, outputArray);
-};
+    this.processLinks(text, outputArray);
+  };
+
+  window.GrLinkTextParser = GrLinkTextParser;
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c081b30..cf680d2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -57,6 +57,31 @@
    */
   Defs.ChangeFetchRequest;
 
+  /**
+   * Object to describe a request for passing into _send.
+   * - method is the HTTP method to use in the request.
+   * - url is the URL for the request
+   * - body is a request payload.
+   *     TODO (beckysiegel) remove need for number at least.
+   * - errFn is a function to invoke when the request fails.
+   * - cancelCondition is a function that, if provided and returns true, will
+   *   cancel the response after it resolves.
+   * - contentType is the content type of the body.
+   * - headers is a key-value hash to describe HTTP headers for the request.
+   * - parseResponse states whether the result should be parsed as a JSON
+   *     object using getResponseObject.
+   * @typedef {{
+   *   method: string,
+   *   url: string,
+   *   body: (string|number|Object|null|undefined),
+   *   errFn: (function(?Response, string=)|null|undefined),
+   *   contentType: (string|null|undefined),
+   *   headers: (Object|undefined),
+   *   parseResponse: (boolean|undefined),
+   * }}
+   */
+  Defs.SendRequest;
+
   const DiffViewMode = {
     SIDE_BY_SIDE: 'SIDE_BY_SIDE',
     UNIFIED: 'UNIFIED_DIFF',
@@ -138,16 +163,50 @@
     JSON_PREFIX,
 
     /**
+     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
+     * with timing and logging.
+     * @param {string} url
+     * @param {Object=} opt_fetchOptions
+     */
+    _fetch(url, opt_fetchOptions) {
+      const start = Date.now();
+      const xhr = this._auth.fetch(url, opt_fetchOptions);
+
+      // Log the call after it completes.
+      xhr.then(res => this._logCall(url, opt_fetchOptions, start, res.status));
+
+      // Return the XHR directly (without the log).
+      return xhr;
+    },
+
+    /**
+     * Log information about a REST call. Because the elapsed time is determined
+     * by this method, it should be called immediately after the request
+     * finishes.
+     * @param {string} url
+     * @param {Object|undefined} fetchOptions
+     * @param {number} startTime the time that the request was started.
+     * @param {number} status the HTTP status of the response. The status value
+     *     is used here rather than the response object so there is no way this
+     *     method can read the body stream.
+     */
+    _logCall(url, fetchOptions, startTime, status) {
+      const method = (fetchOptions && fetchOptions.method) ?
+          fetchOptions.method : 'GET';
+      const elapsed = (Date.now() - startTime) + 'ms';
+      console.log(['HTTP', status, method, elapsed, url].join(' '));
+    },
+
+    /**
      * Fetch JSON from url provided.
      * Returns a Promise that resolves to a native Response.
      * Doesn't do error checking. Supports cancel condition. Performs auth.
      * Validates auth expiry errors.
      * @param {Defs.FetchJSONRequest} req
-     * @return {Promise}
      */
     _fetchRawJSON(req) {
       const urlWithParams = this._urlWithParams(req.url, req.params);
-      return this._auth.fetch(urlWithParams, req.fetchOptions).then(res => {
+      return this._fetch(urlWithParams, req.fetchOptions).then(res => {
         if (req.cancelCondition && req.cancelCondition()) {
           res.body.cancel();
           return;
@@ -297,8 +356,12 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(repo);
-      return this.send('PUT', `/projects/${encodeName}/config`, config,
-          opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: `/projects/${encodeName}/config`,
+        body: config,
+        errFn: opt_errFn,
+      });
     },
 
     runRepoGC(repo, opt_errFn) {
@@ -306,7 +369,12 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(repo);
-      return this.send('POST', `/projects/${encodeName}/gc`, '', opt_errFn);
+      return this._send({
+        method: 'POST',
+        url: `/projects/${encodeName}/gc`,
+        body: '',
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -318,7 +386,12 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(config.name);
-      return this.send('PUT', `/projects/${encodeName}`, config, opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: `/projects/${encodeName}`,
+        body: config,
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -328,7 +401,12 @@
     createGroup(config, opt_errFn) {
       if (!config.name) { return ''; }
       const encodeName = encodeURIComponent(config.name);
-      return this.send('PUT', `/groups/${encodeName}`, config, opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: `/groups/${encodeName}`,
+        body: config,
+        errFn: opt_errFn,
+      });
     },
 
     getGroupConfig(group, opt_errFn) {
@@ -349,8 +427,12 @@
       // supports it.
       const encodeName = encodeURIComponent(repo);
       const encodeRef = encodeURIComponent(ref);
-      return this.send('DELETE',
-          `/projects/${encodeName}/branches/${encodeRef}`, '', opt_errFn);
+      return this._send({
+        method: 'DELETE',
+        url: `/projects/${encodeName}/branches/${encodeRef}`,
+        body: '',
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -364,8 +446,12 @@
       // supports it.
       const encodeName = encodeURIComponent(repo);
       const encodeRef = encodeURIComponent(ref);
-      return this.send('DELETE',
-          `/projects/${encodeName}/tags/${encodeRef}`, '', opt_errFn);
+      return this._send({
+        method: 'DELETE',
+        url: `/projects/${encodeName}/tags/${encodeRef}`,
+        body: '',
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -380,9 +466,12 @@
       // supports it.
       const encodeName = encodeURIComponent(name);
       const encodeBranch = encodeURIComponent(branch);
-      return this.send('PUT',
-          `/projects/${encodeName}/branches/${encodeBranch}`,
-          revision, opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: `/projects/${encodeName}/branches/${encodeBranch}`,
+        body: revision,
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -397,8 +486,12 @@
       // supports it.
       const encodeName = encodeURIComponent(name);
       const encodeTag = encodeURIComponent(tag);
-      return this.send('PUT', `/projects/${encodeName}/tags/${encodeTag}`,
-          revision, opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: `/projects/${encodeName}/tags/${encodeTag}`,
+        body: revision,
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -413,35 +506,51 @@
 
     getGroupMembers(groupName, opt_errFn) {
       const encodeName = encodeURIComponent(groupName);
-      return this.send('GET', `/groups/${encodeName}/members/`, null, opt_errFn)
-          .then(response => this.getResponseObject(response));
+      return this._fetchJSON({
+        url: `/groups/${encodeName}/members/`,
+        errFn: opt_errFn,
+      });
     },
 
     getIncludedGroup(groupName) {
       const encodeName = encodeURIComponent(groupName);
-      return this.send('GET', `/groups/${encodeName}/groups/`)
-          .then(response => this.getResponseObject(response));
+      return this._fetchJSON({url: `/groups/${encodeName}/groups/`});
     },
 
     saveGroupName(groupId, name) {
       const encodeId = encodeURIComponent(groupId);
-      return this.send('PUT', `/groups/${encodeId}/name`, {name});
+      return this._send({
+        method: 'PUT',
+        url: `/groups/${encodeId}/name`,
+        body: {name},
+      });
     },
 
     saveGroupOwner(groupId, ownerId) {
       const encodeId = encodeURIComponent(groupId);
-      return this.send('PUT', `/groups/${encodeId}/owner`, {owner: ownerId});
+      return this._send({
+        method: 'PUT',
+        url: `/groups/${encodeId}/owner`,
+        body: {owner: ownerId},
+      });
     },
 
     saveGroupDescription(groupId, description) {
       const encodeId = encodeURIComponent(groupId);
-      return this.send('PUT', `/groups/${encodeId}/description`,
-          {description});
+      return this._send({
+        method: 'PUT',
+        url: `/groups/${encodeId}/description`,
+        body: {description},
+      });
     },
 
     saveGroupOptions(groupId, options) {
       const encodeId = encodeURIComponent(groupId);
-      return this.send('PUT', `/groups/${encodeId}/options`, options);
+      return this._send({
+        method: 'PUT',
+        url: `/groups/${encodeId}/options`,
+        body: options,
+      });
     },
 
     getGroupAuditLog(group, opt_errFn) {
@@ -454,34 +563,44 @@
     saveGroupMembers(groupName, groupMembers) {
       const encodeName = encodeURIComponent(groupName);
       const encodeMember = encodeURIComponent(groupMembers);
-      return this.send('PUT', `/groups/${encodeName}/members/${encodeMember}`)
-          .then(response => this.getResponseObject(response));
+      return this._send({
+        method: 'PUT',
+        url: `/groups/${encodeName}/members/${encodeMember}`,
+        parseResponse: true,
+      });
     },
 
     saveIncludedGroup(groupName, includedGroup, opt_errFn) {
       const encodeName = encodeURIComponent(groupName);
       const encodeIncludedGroup = encodeURIComponent(includedGroup);
-      return this.send('PUT',
-          `/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
-          opt_errFn).then(response => {
-            if (response.ok) {
-              return this.getResponseObject(response);
-            }
-          });
+      const req = {
+        method: 'PUT',
+        url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+        errFn: opt_errFn,
+      };
+      return this._send(req).then(response => {
+        if (response.ok) {
+          return this.getResponseObject(response);
+        }
+      });
     },
 
     deleteGroupMembers(groupName, groupMembers) {
       const encodeName = encodeURIComponent(groupName);
       const encodeMember = encodeURIComponent(groupMembers);
-      return this.send('DELETE',
-          `/groups/${encodeName}/members/${encodeMember}`);
+      return this._send({
+        method: 'DELETE',
+        url: `/groups/${encodeName}/members/${encodeMember}`,
+      });
     },
 
     deleteIncludedGroup(groupName, includedGroup) {
       const encodeName = encodeURIComponent(groupName);
       const encodeIncludedGroup = encodeURIComponent(includedGroup);
-      return this.send('DELETE',
-          `/groups/${encodeName}/groups/${encodeIncludedGroup}`);
+      return this._send({
+        method: 'DELETE',
+        url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+      });
     },
 
     getVersion() {
@@ -559,7 +678,12 @@
         prefs.download_scheme = prefs.download_scheme.toLowerCase();
       }
 
-      return this.send('PUT', '/accounts/self/preferences', prefs, opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: '/accounts/self/preferences',
+        body: prefs,
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -569,8 +693,12 @@
     saveDiffPreferences(prefs, opt_errFn) {
       // Invalidate the cache.
       this._cache['/accounts/self/preferences.diff'] = undefined;
-      return this.send('PUT', '/accounts/self/preferences.diff', prefs,
-          opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: '/accounts/self/preferences.diff',
+        body: prefs,
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -580,8 +708,12 @@
     saveEditPreferences(prefs, opt_errFn) {
       // Invalidate the cache.
       this._cache['/accounts/self/preferences.edit'] = undefined;
-      return this.send('PUT', '/accounts/self/preferences.edit', prefs,
-          opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: '/accounts/self/preferences.edit',
+        body: prefs,
+        errFn: opt_errFn,
+      });
     },
 
     getAccount() {
@@ -600,8 +732,12 @@
     },
 
     deleteAccountIdentity(id) {
-      return this.send('POST', '/accounts/self/external.ids:delete', id)
-          .then(response => this.getResponseObject(response));
+      return this._send({
+        method: 'POST',
+        url: '/accounts/self/external.ids:delete',
+        body: id,
+        parseResponse: true,
+      });
     },
 
     /**
@@ -623,8 +759,11 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     addAccountEmail(email, opt_errFn) {
-      return this.send('PUT', '/accounts/self/emails/' +
-          encodeURIComponent(email), null, opt_errFn);
+      return this._send({
+        method: 'PUT',
+        url: '/accounts/self/emails/' + encodeURIComponent(email),
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -632,8 +771,11 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     deleteAccountEmail(email, opt_errFn) {
-      return this.send('DELETE', '/accounts/self/emails/' +
-          encodeURIComponent(email), null, opt_errFn);
+      return this._send({
+        method: 'DELETE',
+        url: '/accounts/self/emails/' + encodeURIComponent(email),
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -643,7 +785,7 @@
     setPreferredAccountEmail(email, opt_errFn) {
       const encodedEmail = encodeURIComponent(email);
       const url = `/accounts/self/emails/${encodedEmail}/preferred`;
-      return this.send('PUT', url, null, opt_errFn).then(() => {
+      return this._send({method: 'PUT', url, errFn: opt_errFn}).then(() => {
         // If result of getAccountEmails is in cache, update it in the cache
         // so we don't have to invalidate it.
         const cachedEmails = this._cache['/accounts/self/emails'];
@@ -679,8 +821,14 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     setAccountName(name, opt_errFn) {
-      return this.send('PUT', '/accounts/self/name', {name}, opt_errFn)
-          .then(response => this.getResponseObject(response))
+      const req = {
+        method: 'PUT',
+        url: '/accounts/self/name',
+        body: {name},
+        errFn: opt_errFn,
+        parseResponse: true,
+      };
+      return this._send(req)
           .then(newName => this._updateCachedAccount({name: newName}));
     },
 
@@ -689,8 +837,14 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     setAccountUsername(username, opt_errFn) {
-      return this.send('PUT', '/accounts/self/username', {username}, opt_errFn)
-          .then(response => this.getResponseObject(response))
+      const req = {
+        method: 'PUT',
+        url: '/accounts/self/username',
+        body: {username},
+        errFn: opt_errFn,
+        parseResponse: true,
+      };
+      return this._send(req)
           .then(newName => this._updateCachedAccount({username: newName}));
     },
 
@@ -699,8 +853,14 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     setAccountStatus(status, opt_errFn) {
-      return this.send('PUT', '/accounts/self/status', {status}, opt_errFn)
-          .then(response => this.getResponseObject(response))
+      const req = {
+        method: 'PUT',
+        url: '/accounts/self/status',
+        body: {status},
+        errFn: opt_errFn,
+        parseResponse: true,
+      };
+      return this._send(req)
           .then(newStatus => this._updateCachedAccount({status: newStatus}));
     },
 
@@ -719,7 +879,11 @@
     },
 
     saveAccountAgreement(name) {
-      return this.send('PUT', '/accounts/self/agreements', name);
+      return this._send({
+        method: 'PUT',
+        url: '/accounts/self/agreements',
+        body: name,
+      });
     },
 
     /**
@@ -812,9 +976,13 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     saveWatchedProjects(projects, opt_errFn) {
-      const url = '/accounts/self/watched.projects';
-      return this.send('POST', url, projects, opt_errFn)
-          .then(response => this.getResponseObject(response));
+      return this._send({
+        method: 'POST',
+        url: '/accounts/self/watched.projects',
+        body: projects,
+        errFn: opt_errFn,
+        parseResponse: true,
+      });
     },
 
     /**
@@ -822,8 +990,12 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     deleteWatchedProjects(projects, opt_errFn) {
-      return this.send('POST', '/accounts/self/watched.projects:delete',
-          projects, opt_errFn);
+      return this._send({
+        method: 'POST',
+        url: '/accounts/self/watched.projects:delete',
+        body: projects,
+        errFn: opt_errFn,
+      });
     },
 
     /**
@@ -1176,8 +1348,11 @@
     setRepoHead(repo, ref) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this.send(
-          'PUT', `/projects/${encodeURIComponent(repo)}/HEAD`, {ref});
+      return this._send({
+        method: 'PUT',
+        url: `/projects/${encodeURIComponent(repo)}/HEAD`,
+        body: {ref},
+      });
     },
 
     /**
@@ -1246,15 +1421,20 @@
     setRepoAccessRights(repoName, repoInfo) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this.send(
-          'POST', `/projects/${encodeURIComponent(repoName)}/access`,
-          repoInfo);
+      return this._send({
+        method: 'POST',
+        url: `/projects/${encodeURIComponent(repoName)}/access`,
+        body: repoInfo,
+      });
     },
 
     setRepoAccessRightsForReview(projectName, projectInfo) {
-      return this.send(
-          'PUT', `/projects/${encodeURIComponent(projectName)}/access:review`,
-          projectInfo).then(response => this.getResponseObject(response));
+      return this._send({
+        method: 'PUT',
+        url: `/projects/${encodeURIComponent(projectName)}/access:review`,
+        body: projectInfo,
+        parseResponse: true,
+      });
     },
 
     /**
@@ -1332,7 +1512,7 @@
                 throw Error('Unsupported HTTP method: ' + method);
             }
 
-            return this.send(method, url, body);
+            return this._send({method, url, body});
           });
     },
 
@@ -1429,7 +1609,12 @@
         this.getChangeActionURL(changeNum, patchNum, '/review'),
       ];
       return Promise.all(promises).then(([, url]) => {
-        return this.send('POST', url, review, opt_errFn);
+        return this._send({
+          method: 'POST',
+          url,
+          body: review,
+          errFn: opt_errFn,
+        });
       });
     },
 
@@ -1457,16 +1642,21 @@
      */
     createChange(project, branch, subject, opt_topic, opt_isPrivate,
         opt_workInProgress, opt_baseChange, opt_baseCommit) {
-      return this.send('POST', '/changes/', {
-        project,
-        branch,
-        subject,
-        topic: opt_topic,
-        is_private: opt_isPrivate,
-        work_in_progress: opt_workInProgress,
-        base_change: opt_baseChange,
-        base_commit: opt_baseCommit,
-      }).then(response => this.getResponseObject(response));
+      return this._send({
+        method: 'POST',
+        url: '/changes/',
+        body: {
+          project,
+          branch,
+          subject,
+          topic: opt_topic,
+          is_private: opt_isPrivate,
+          work_in_progress: opt_workInProgress,
+          base_change: opt_baseChange,
+          base_commit: opt_baseCommit,
+        },
+        parseResponse: true,
+      });
     },
 
     /**
@@ -1572,10 +1762,58 @@
     saveChangeStarred(changeNum, starred) {
       const url = '/accounts/self/starred.changes/' + changeNum;
       const method = starred ? 'PUT' : 'DELETE';
-      return this.send(method, url);
+      return this._send({method, url});
     },
 
     /**
+     * Send an XHR.
+     * @param {Defs.SendRequest} req
+     * @return {Promise}
+     */
+    _send(req) {
+      const options = {method: req.method};
+      if (req.body) {
+        options.headers = new Headers();
+        options.headers.set(
+            'Content-Type', req.contentType || 'application/json');
+        options.body = typeof req.body === 'string' ?
+            req.body : JSON.stringify(req.body);
+      }
+      if (req.headers) {
+        if (!options.headers) { options.headers = new Headers(); }
+        for (const header in req.headers) {
+          if (!req.headers.hasOwnProperty(header)) { continue; }
+          options.headers.set(header, req.headers[header]);
+        }
+      }
+      const url = req.url.startsWith('http') ?
+          req.url : this.getBaseUrl() + req.url;
+      const xhr = this._fetch(url, options).then(response => {
+        if (!response.ok) {
+          if (req.errFn) {
+            return req.errFn.call(undefined, response);
+          }
+          this.fire('server-error', {response});
+        }
+        return response;
+      }).catch(err => {
+        this.fire('network-error', {error: err});
+        if (req.errFn) {
+          return req.errFn.call(undefined, null, err);
+        } else {
+          throw err;
+        }
+      });
+
+      if (req.parseResponse) {
+        return xhr.then(res => this.getResponseObject(res));
+      }
+
+      return xhr;
+    },
+
+    /**
+     * Public version of the _send method preserved for plugins.
      * @param {string} method
      * @param {string} url
      * @param {?string|number|Object=} opt_body passed as null sometimes
@@ -1586,42 +1824,15 @@
      * @param {?string=} opt_contentType
      * @param {Object=} opt_headers
      */
-    send(method, url, opt_body, opt_errFn, opt_contentType, opt_headers) {
-      const options = {method};
-      if (opt_body) {
-        options.headers = new Headers();
-        options.headers.set(
-            'Content-Type', opt_contentType || 'application/json');
-        if (typeof opt_body !== 'string') {
-          opt_body = JSON.stringify(opt_body);
-        }
-        options.body = opt_body;
-      }
-      if (opt_headers) {
-        if (!options.headers) { options.headers = new Headers(); }
-        for (const header in opt_headers) {
-          if (!opt_headers.hasOwnProperty(header)) { continue; }
-          options.headers.set(header, opt_headers[header]);
-        }
-      }
-      if (!url.startsWith('http')) {
-        url = this.getBaseUrl() + url;
-      }
-      return this._auth.fetch(url, options).then(response => {
-        if (!response.ok) {
-          if (opt_errFn) {
-            return opt_errFn.call(null, response);
-          }
-          this.fire('server-error', {response});
-        }
-        return response;
-      }).catch(err => {
-        this.fire('network-error', {error: err});
-        if (opt_errFn) {
-          return opt_errFn.call(null, null, err);
-        } else {
-          throw err;
-        }
+    send(method, url, opt_body, opt_errFn, opt_contentType,
+        opt_headers) {
+      return this._send({
+        method,
+        url,
+        body: opt_body,
+        errFn: opt_errFn,
+        contentType: opt_contentType,
+        headers: opt_headers,
       });
     },
 
@@ -1867,7 +2078,7 @@
     },
 
     _fetchB64File(url) {
-      return this._auth.fetch(this.getBaseUrl() + url)
+      return this._fetch(this.getBaseUrl() + url)
           .then(response => {
             if (!response.ok) { return Promise.reject(response.statusText); }
             const type = response.headers.get('X-FYI-Content-Type');
@@ -1978,7 +2189,10 @@
     },
 
     deleteAccountHttpPassword() {
-      return this.send('DELETE', '/accounts/self/password.http');
+      return this._send({
+        method: 'DELETE',
+        url: '/accounts/self/password.http',
+      });
     },
 
     /**
@@ -1987,8 +2201,12 @@
      * parameter.
      */
     generateAccountHttpPassword() {
-      return this.send('PUT', '/accounts/self/password.http', {generate: true})
-          .then(this.getResponseObject.bind(this));
+      return this._send({
+        method: 'PUT',
+        url: '/accounts/self/password.http',
+        body: {generate: true},
+        parseResponse: true,
+      });
     },
 
     getAccountSSHKeys() {
@@ -1996,8 +2214,13 @@
     },
 
     addAccountSSHKey(key) {
-      return this.send('POST', '/accounts/self/sshkeys', key, null,
-          'plain/text')
+      const req = {
+        method: 'POST',
+        url: '/accounts/self/sshkeys',
+        body: key,
+        contentType: 'plain/text',
+      };
+      return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
               return Promise.reject();
@@ -2011,7 +2234,10 @@
     },
 
     deleteAccountSSHKey(id) {
-      return this.send('DELETE', '/accounts/self/sshkeys/' + id);
+      return this._send({
+        method: 'DELETE',
+        url: '/accounts/self/sshkeys/' + id,
+      });
     },
 
     getAccountGPGKeys() {
@@ -2019,7 +2245,8 @@
     },
 
     addAccountGPGKey(key) {
-      return this.send('POST', '/accounts/self/gpgkeys', key)
+      const req = {method: 'POST', url: '/accounts/self/gpgkeys', body: key};
+      return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
               return Promise.reject();
@@ -2033,7 +2260,10 @@
     },
 
     deleteAccountGPGKey(id) {
-      return this.send('DELETE', '/accounts/self/gpgkeys/' + id);
+      return this._send({
+        method: 'DELETE',
+        url: '/accounts/self/gpgkeys/' + id,
+      });
     },
 
     deleteVote(changeNum, account, label) {
@@ -2048,13 +2278,17 @@
     },
 
     confirmEmail(token) {
-      return this.send('PUT', '/config/server/email.confirm', {token})
-          .then(response => {
-            if (response.status === 204) {
-              return 'Email confirmed successfully.';
-            }
-            return null;
-          });
+      const req = {
+        method: 'PUT',
+        url: '/config/server/email.confirm',
+        body: {token},
+      };
+      return this._send(req).then(response => {
+        if (response.status === 204) {
+          return 'Email confirmed successfully.';
+        }
+        return null;
+      });
     },
 
     getCapabilities(token, opt_errFn) {
@@ -2190,9 +2424,16 @@
      */
     getChangeURLAndSend(changeNum, method, patchNum, endpoint, opt_payload,
         opt_errFn, opt_contentType, opt_headers) {
-      return this._changeBaseURL(changeNum, patchNum).then(url =>
-        this.send(method, url + endpoint, opt_payload, opt_errFn,
-            opt_contentType, opt_headers));
+      return this._changeBaseURL(changeNum, patchNum).then(url => {
+        return this._send({
+          method,
+          url: url + endpoint,
+          body: opt_payload,
+          errFn: opt_errFn,
+          contentType: opt_contentType,
+          headers: opt_headers,
+        });
+      });
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 7e71efa..5ef4c41 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -494,10 +494,10 @@
 
     test('saveDiffPreferences invalidates cache line', () => {
       const cacheKey = '/accounts/self/preferences.diff';
-      sandbox.stub(element, 'send');
+      sandbox.stub(element, '_send');
       element._cache[cacheKey] = {tab_size: 4};
       element.saveDiffPreferences({tab_size: 8});
-      assert.isTrue(element.send.called);
+      assert.isTrue(element._send.called);
       assert.notOk(element._cache[cacheKey]);
     });
 
@@ -619,10 +619,10 @@
         });
 
     test('savPreferences normalizes download scheme', () => {
-      sandbox.stub(element, 'send');
+      sandbox.stub(element, '_send');
       element.savePreferences({download_scheme: 'HTTP'});
-      assert.isTrue(element.send.called);
-      assert.equal(element.send.lastCall.args[2].download_scheme, 'http');
+      assert.isTrue(element._send.called);
+      assert.equal(element._send.lastCall.args[0].body.download_scheme, 'http');
     });
 
     test('getDiffPreferences returns correct defaults', done => {
@@ -648,10 +648,10 @@
     });
 
     test('saveDiffPreferences set show_tabs to false', () => {
-      sandbox.stub(element, 'send');
+      sandbox.stub(element, '_send');
       element.saveDiffPreferences({show_tabs: false});
-      assert.isTrue(element.send.called);
-      assert.equal(element.send.lastCall.args[2].show_tabs, false);
+      assert.isTrue(element._send.called);
+      assert.equal(element._send.lastCall.args[0].body.show_tabs, false);
     });
 
     test('getEditPreferences returns correct defaults', done => {
@@ -681,17 +681,21 @@
     });
 
     test('saveEditPreferences set show_tabs to false', () => {
-      sandbox.stub(element, 'send');
+      sandbox.stub(element, '_send');
       element.saveEditPreferences({show_tabs: false});
-      assert.isTrue(element.send.called);
-      assert.equal(element.send.lastCall.args[2].show_tabs, false);
+      assert.isTrue(element._send.called);
+      assert.equal(element._send.lastCall.args[0].body.show_tabs, false);
     });
 
     test('confirmEmail', () => {
-      sandbox.spy(element, 'send');
+      sandbox.spy(element, '_send');
       element.confirmEmail('foo');
-      assert.isTrue(element.send.calledWith(
-          'PUT', '/config/server/email.confirm', {token: 'foo'}));
+      assert.isTrue(element._send.calledOnce);
+      assert.deepEqual(element._send.lastCall.args[0], {
+        method: 'PUT',
+        url: '/config/server/email.confirm',
+        body: {token: 'foo'},
+      });
     });
 
     test('GrReviewerUpdatesParser.parse is used', () => {
@@ -703,17 +707,18 @@
       });
     });
 
-    test('setAccountStatus', done => {
-      sandbox.stub(element, 'send').returns(Promise.resolve('OOO'));
-      sandbox.stub(element, 'getResponseObject')
-          .returns(Promise.resolve('OOO'));
+    test('setAccountStatus', () => {
+      sandbox.stub(element, '_send').returns(Promise.resolve('OOO'));
       element._cache['/accounts/self/detail'] = {};
-      element.setAccountStatus('OOO').then(() => {
-        assert.isTrue(element.send.calledWith('PUT', '/accounts/self/status',
-            {status: 'OOO'}));
+      return element.setAccountStatus('OOO').then(() => {
+        assert.isTrue(element._send.calledOnce);
+        assert.equal(element._send.lastCall.args[0].method, 'PUT');
+        assert.equal(element._send.lastCall.args[0].url,
+            '/accounts/self/status');
+        assert.deepEqual(element._send.lastCall.args[0].body,
+            {status: 'OOO'});
         assert.deepEqual(element._cache['/accounts/self/detail'],
             {status: 'OOO'});
-        done();
       });
     });
 
@@ -797,39 +802,41 @@
       });
     });
 
-    test('saveChangeEdit', done => {
+    test('saveChangeEdit', () => {
       element._projectLookup = {1: 'test'};
       const change_num = '1';
       const file_name = 'index.php';
       const file_contents = '<?php';
-      sandbox.stub(element, 'send').returns(
-          Promise.resolve([change_num, file_name, file_contents])
-      );
+      sandbox.stub(element, '_send').returns(
+          Promise.resolve([change_num, file_name, file_contents]));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve([change_num, file_name, file_contents]));
       element._cache['/changes/' + change_num + '/edit/' + file_name] = {};
-      element.saveChangeEdit(change_num, file_name, file_contents).then(() => {
-        assert.isTrue(element.send.calledWith('PUT',
-            '/changes/test~1/edit/' + file_name,
-            file_contents));
-        done();
-      });
+      return element.saveChangeEdit(change_num, file_name, file_contents)
+          .then(() => {
+            assert.isTrue(element._send.calledOnce);
+            assert.equal(element._send.lastCall.args[0].method, 'PUT');
+            assert.equal(element._send.lastCall.args[0].url,
+                '/changes/test~1/edit/' + file_name);
+            assert.equal(element._send.lastCall.args[0].body, file_contents);
+          });
     });
 
-    test('putChangeCommitMessage', done => {
+    test('putChangeCommitMessage', () => {
       element._projectLookup = {1: 'test'};
       const change_num = '1';
       const message = 'this is a commit message';
-      sandbox.stub(element, 'send').returns(
-          Promise.resolve([change_num, message])
-      );
+      sandbox.stub(element, '_send').returns(
+          Promise.resolve([change_num, message]));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve([change_num, message]));
       element._cache['/changes/' + change_num + '/message'] = {};
-      element.putChangeCommitMessage(change_num, message).then(() => {
-        assert.isTrue(element.send.calledWith('PUT',
-            '/changes/test~1/message', {message}));
-        done();
+      return element.putChangeCommitMessage(change_num, message).then(() => {
+        assert.isTrue(element._send.calledOnce);
+        assert.equal(element._send.lastCall.args[0].method, 'PUT');
+        assert.equal(element._send.lastCall.args[0].url,
+            '/changes/test~1/message');
+        assert.deepEqual(element._send.lastCall.args[0].body, {message});
       });
     });
 
@@ -866,9 +873,12 @@
     });
 
     test('createRepo encodes name', () => {
-      const sendStub = sandbox.stub(element, 'send');
-      element.createRepo({name: 'x/y'});
-      assert.equal(sendStub.lastCall.args[1], '/projects/x%2Fy');
+      const sendStub = sandbox.stub(element, '_send')
+          .returns(Promise.resolve());
+      return element.createRepo({name: 'x/y'}).then(() => {
+        assert.isTrue(sendStub.calledOnce);
+        assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+      });
     });
 
     test('queryChangeFiles', () => {
@@ -1121,10 +1131,13 @@
 
     test('getChangeURLAndSend', () => {
       element._projectLookup = {1: 'test'};
-      const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+      const sendStub = sandbox.stub(element, '_send')
+          .returns(Promise.resolve());
       return element.getChangeURLAndSend(1, 'POST', 1, '/test').then(() => {
-        assert.isTrue(sendStub.calledWith('POST',
-            '/changes/test~1/revisions/1/test'));
+        assert.isTrue(sendStub.calledOnce);
+        assert.equal(sendStub.lastCall.args[0].method, 'POST');
+        assert.equal(sendStub.lastCall.args[0].url,
+            '/changes/test~1/revisions/1/test');
       });
     });
 
@@ -1164,10 +1177,10 @@
     });
 
     test('generateAccountHttpPassword', () => {
-      const sendSpy = sandbox.spy(element, 'send');
+      const sendSpy = sandbox.spy(element, '_send');
       return element.generateAccountHttpPassword().then(() => {
         assert.isTrue(sendSpy.calledOnce);
-        assert.deepEqual(sendSpy.lastCall.args[2], {generate: true});
+        assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
       });
     });
 
@@ -1319,5 +1332,21 @@
         });
       });
     });
+
+    test('_fetch forwards request and logs', () => {
+      const logStub = sandbox.stub(element, '_logCall');
+      const response = {status: 404, text: sinon.stub()};
+      const url = 'my url';
+      const fetchOptions = {method: 'DELETE'};
+      sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
+      const startTime = 123;
+      sandbox.stub(Date, 'now').returns(startTime);
+      return element._fetch(url, fetchOptions).then(() => {
+        assert.isTrue(logStub.calledOnce);
+        assert.isTrue(logStub.calledWith(
+            url, fetchOptions, startTime, response.status));
+        assert.isFalse(response.text.called);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 7379b9c..6d7469b 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -68,7 +68,6 @@
       }
       .topHeader th {
         background-color: var(--table-header-background-color);
-        font-size: var(--font-size-large);
         height: 3rem;
         position: -webkit-sticky;
         position: sticky;
@@ -140,12 +139,12 @@
       .truncatedProject {
         display: none;
       }
-      @media only screen and (max-width: 100em) {
+      @media only screen and (max-width: 150em) {
         .assignee,
         .branch,
         .owner {
           overflow: hidden;
-          max-width: 10rem;
+          max-width: 18rem;
           text-overflow: ellipsis;
         }
         .truncatedProject {
@@ -155,6 +154,13 @@
           display: none;
         }
       }
+      @media only screen and (max-width: 100em) {
+        .assignee,
+        .branch,
+        .owner {
+          max-width: 10rem;
+        }
+      }
       @media only screen and (max-width: 50em) {
         :host {
           font-size: var(--font-size-large);
diff --git a/polygerrit-ui/app/styles/gr-table-styles.html b/polygerrit-ui/app/styles/gr-table-styles.html
index 5e40735..79d8100 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.html
+++ b/polygerrit-ui/app/styles/gr-table-styles.html
@@ -30,6 +30,9 @@
       .genericList tr {
         border-bottom: 1px solid var(--border-color);
       }
+      .genericList tr:hover {
+        background-color: var(--hover-background-color);
+      }
       .genericList th {
         white-space: nowrap;
       }
@@ -71,11 +74,11 @@
       }
       .genericList .topHeader {
         background-color: var(--table-header-background-color);
-        font-size: var(--font-size-large);
         height: 3rem;
       }
       .genericList .groupHeader {
         background-color: var(--table-subheader-background-color);
+        font-size: var(--font-size-large);
       }
       .genericList a {
         color: var(--primary-text-color);
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index 69262c9..21db329 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -42,7 +42,7 @@
   --table-header-background-color: #fafafa;
   --table-subheader-background-color: #eaeaea;
 
-  --chip-background-color: var(--header-background-color);
+  --chip-background-color: #eee;
 
   --dropdown-background-color: #fff;
 
@@ -53,7 +53,7 @@
   /* Font sizes */
   --font-size-normal: 1rem;
   --font-size-small: .92rem;
-  --font-size-large: 1.076rem;
+  --font-size-large: 1.154rem;
 
   --link-color: #2a66d9;
   --primary-button-background-color: var(--link-color);
@@ -78,14 +78,14 @@
 
   /* Diff colors */
   --diff-selection-background-color: #c7dbf9;
-  --light-remove-highlight-color: #fee;
-  --light-add-highlight-color: #efe;
-  --light-remove-add-highlight-color: #fff6ea;
-  --light-rebased-add-highlight-color: #edfffa;
-  --dark-remove-highlight-color: rgba(255, 0, 0, 0.15);
-  --dark-add-highlight-color: rgba(0, 255, 0, 0.15);
-  --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
-  --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
+  --light-remove-highlight-color: #FFEBEE;
+  --light-add-highlight-color: #D8FED8;
+  --light-remove-add-highlight-color: #FFF8DC;
+  --light-rebased-add-highlight-color: #EEEEFF;
+  --dark-remove-highlight-color: #FFCDD2;
+  --dark-add-highlight-color: #AAF2AA;
+  --dark-rebased-remove-highlight-color: #F7E8B7;
+  --dark-rebased-add-highlight-color: #D7D7F9;
   --diff-context-control-color: #fff7d4;
   --diff-context-control-border-color: #f6e6a5;
   --diff-tab-indicator-color: var(--deemphasized-text-color);
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 6a562fc..5a5dbcd 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -112,7 +112,6 @@
     'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
     'diff/gr-syntax-layer/gr-syntax-layer_test.html',
-    'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
     'edit/gr-default-editor/gr-default-editor_test.html',
     'edit/gr-edit-controls/gr-edit-controls_test.html',
     'edit/gr-edit-file-controls/gr-edit-file-controls_test.html',
@@ -165,6 +164,7 @@
     'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
     'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
     'shared/gr-fixed-panel/gr-fixed-panel_test.html',
+    'shared/gr-lib-loader/gr-lib-loader_test.html',
     'shared/gr-limited-text/gr-limited-text_test.html',
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
diff --git a/proto/cache.proto b/proto/cache.proto
index 7e2e75a..a826f8c 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -184,3 +184,12 @@
   int64 read_only_until = 17;
   bool has_read_only_until = 18;
 }
+
+
+// Serialized form of com.google.gerrit.server.query.change.ConflictKey
+message ConflictKeyProto {
+  bytes commit = 1;
+  bytes other_commit = 2;
+  string submit_type = 3;
+  bool content_merge = 4;
+}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 699dd0e..3dd6360 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -21,6 +21,7 @@
  * @param staticResourcePath
  * @param? faviconPath
  * @param? versionInfo
+ * @param? deprecateGwtUi
  */
 {template .Index}
   <!DOCTYPE html>{\n}
@@ -32,7 +33,9 @@
   <script>
     window.CLOSURE_NO_DEPS = true;
     {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
+    {if $deprecateGwtUi}window.DEPRECATE_GWT_UI = true;{/if}
     {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
+    {if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
   </script>{\n}
 
   {if $faviconPath}
diff --git a/tools/bazel.rc b/tools/bazel.rc
index ab974d9..7230cf3 100644
--- a/tools/bazel.rc
+++ b/tools/bazel.rc
@@ -1,2 +1,6 @@
 build --workspace_status_command=./tools/workspace-status.sh --strategy=Closure=worker
+build --disk_cache=~/.gerritcodereview/bazel-cache/cas
+build --repository_cache=~/.gerritcodereview/bazel-cache/repository
+build --experimental_strict_action_env
+build --action_env=PATH
 test --build_tests_only
diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl
index 62fa4c6..e20624d 100644
--- a/tools/bzl/asciidoc.bzl
+++ b/tools/bzl/asciidoc.bzl
@@ -103,7 +103,7 @@
 
 _asciidoc_attrs = {
     "_exe": attr.label(
-        default = Label("//lib/asciidoctor:asciidoc"),
+        default = Label("//java/com/google/gerrit/asciidoctor:asciidoc"),
         cfg = "host",
         allow_files = True,
         executable = True,
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 8ef8b7b..23f88df 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -28,6 +28,7 @@
     gwt_module = [],
     resources = [],
     manifest_entries = [],
+    dir_name = None,
     target_suffix = "",
     **kwargs):
   native.java_library(
@@ -43,6 +44,9 @@
   if gwt_module:
     static_jars = [':%s-static' % name]
 
+  if not dir_name:
+    dir_name = name
+
   native.java_binary(
     name = '%s__non_stamped' % name,
     deploy_manifest_lines = manifest_entries + ["Gerrit-ApiType: plugin"],
@@ -88,7 +92,7 @@
     stamp = 1,
     srcs = ['%s__non_stamped_deploy.jar' % name],
     cmd = " && ".join([
-      "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % name.upper(),
+      "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
       "cd $$TMP",
       "unzip -q $$ROOT/$<",
       "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 67763e2..22c1a80 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -9,6 +9,7 @@
 
 TEST_DEPS = [
     "//gerrit-gwtui:ui_tests",
+    "//javatests/com/google/gerrit/elasticsearch:elasticsearch_test_utils",
     "//javatests/com/google/gerrit/server:server_tests",
 ]
 
@@ -18,8 +19,8 @@
     "//gerrit-plugin-gwtui:gwtui-api-lib",
     "//java/com/google/gerrit/acceptance:lib",
     "//java/com/google/gerrit/server",
-    "//lib/asciidoctor:asciidoc_lib",
-    "//lib/asciidoctor:doc_indexer_lib",
+    "//java/com/google/gerrit/asciidoctor:asciidoc_lib",
+    "//java/com/google/gerrit/asciidoctor:doc_indexer_lib",
     "//lib/auto:auto-value",
     "//lib/gwt:ant",
     "//lib/gwt:colt",
diff --git a/tools/setup_gjf.sh b/tools/setup_gjf.sh
index 9c36c72..de2e0cc 100755
--- a/tools/setup_gjf.sh
+++ b/tools/setup_gjf.sh
@@ -17,7 +17,7 @@
 set -eu
 
 # Keep this version in sync with dev-contributing.txt.
-VERSION=${1:-1.5}
+VERSION=${1:-1.6}
 
 case "$VERSION" in
 1.3)
@@ -26,6 +26,9 @@
 1.5)
     SHA1="b1f79e4d39a3c501f07c0ce7e8b03ac6964ed1f1"
     ;;
+1.6)
+    SHA1="02b3e84e52d2473e2c4868189709905a51647d03"
+    ;;
 *)
     echo "unknown google-java-format version: $VERSION"
     exit 1