Merge "Update apache mina sshd to 1.7.0"
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 48f4b17..d40e7d3 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:
 
 ----
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/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/WORKSPACE b/WORKSPACE
index 74832af..9420a74 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -11,12 +11,11 @@
     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",
+    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
@@ -172,6 +171,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",
@@ -233,9 +252,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 +455,6 @@
     sha1 = "05b6f921f1810bdf90e25471968f741f87168b64",
 )
 
-# When upgrading Lucene, make sure it's compatible with Elasticsearch
 LUCENE_VERS = "5.5.4"
 
 maven_jar(
@@ -470,42 +488,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,
@@ -911,68 +893,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 +908,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 +919,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/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticException.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticException.java
new file mode 100644
index 0000000..d4baf75
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/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/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
new file mode 100644
index 0000000..91f938c
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
@@ -0,0 +1,143 @@
+// 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.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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class ElasticRestClientProvider implements Provider<RestClient>, LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(ElasticRestClientProvider.class);
+
+  private final HttpHost[] hosts;
+  private final String username;
+  private final String password;
+
+  private RestClient client;
+
+  @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();
+          log.info("Elasticsearch integration version {}", 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.
+      }
+    }
+  }
+
+  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();
+      log.info("Connected to Elasticsearch version {}", 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/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersion.java
new file mode 100644
index 0000000..ff26382
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/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/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticVersionTest.java
new file mode 100644
index 0000000..e0da86a
--- /dev/null
+++ b/gerrit-elasticsearch/src/test/java/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/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/asciidoctor/AsciiDoctor.java b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
index 3768ce7..8b432ff 100644
--- a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
+++ b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
@@ -72,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
index b2b0f5d..f5178a0 100644
--- a/java/com/google/gerrit/asciidoctor/BUILD
+++ b/java/com/google/gerrit/asciidoctor/BUILD
@@ -13,8 +13,6 @@
         "//lib:args4j",
         "//lib:guava",
         "//lib/asciidoctor",
-        "//lib/log:api",
-        "//lib/log:nop",
     ],
 )
 
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/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..ee4869b 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -16,29 +16,27 @@
 
 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.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 +44,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 IGNORE_UNMAPPED = "ignore_unmapped";
+  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 +90,40 @@
         .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;
+  private 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;
   }
 
   @Override
@@ -126,7 +133,7 @@
 
   @Override
   public void close() {
-    client.shutdownClient();
+    // Do nothing. Client is closed by the provider.
   }
 
   @Override
@@ -136,77 +143,47 @@
 
   @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()) {
+    String uri = getURI(indexNameRaw, BULK);
+    Response response = postRequest(addActions(c), 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", c, 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()) {
+    Response response = client.get().performRequest("HEAD", indexName);
+    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 addActions(K c);
 
   protected abstract String getMappings();
 
   protected abstract String getId(V v);
 
-  protected Delete delete(String type, K c) {
+  protected String delete(String type, K c) {
     String id = c.toString();
-    return new Delete.Builder(id).index(indexName).type(type).build();
-  }
-
-  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();
-  }
-
-  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();
-    }
+    return new DeleteRequest(id, indexNameRaw, type).toString();
   }
 
   protected abstract V fromDocument(JsonObject doc, Set<String> fields);
@@ -214,16 +191,14 @@
   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("fields").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,19 +218,72 @@
     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");
+    properties.addProperty(IGNORE_UNMAPPED, true);
+
+    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()
@@ -263,13 +291,7 @@
               .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 +309,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 +331,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..1abdee9 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -18,6 +18,9 @@
 
 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,24 +29,20 @@
 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 {
@@ -59,48 +58,48 @@
 
   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.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, ACCOUNTS).add(new UpdateRequest<>(schema, as));
+
+    String uri = getURI(ACCOUNTS, 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 addActions(Account.Id c) {
+    return delete(ACCOUNTS, c);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 58a298e..c7b0fb5 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,18 +63,14 @@
 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>
@@ -95,18 +93,20 @@
   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;
+    this.schema = schema;
     mapping = new ChangeMapping(schema);
   }
 
@@ -127,20 +127,18 @@
       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()) {
+    BulkRequest bulk =
+        new IndexRequest(getId(cd), indexName, insertIndex)
+            .add(new UpdateRequest<>(schema, cd))
+            .add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex));
+
+    String uri = getURI(CHANGES, 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));
     }
   }
 
@@ -156,20 +154,28 @@
       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");
+    properties.addProperty(IGNORE_UNMAPPED, true);
+
+    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 addActions(Id c) {
+    return delete(OPEN_CHANGES, c) + delete(CLOSED_CHANGES, c);
   }
 
   @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..d1eab7c 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -16,6 +16,9 @@
 
 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,24 +26,21 @@
 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 {
@@ -56,48 +56,47 @@
 
   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.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, GROUPS).add(new UpdateRequest<>(schema, group));
+
+    String uri = getURI(GROUPS, 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 addActions(AccountGroup.UUID c) {
+    return delete(GROUPS, c);
   }
 
   @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..bf6da5c 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticMapping.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -26,7 +26,7 @@
     for (FieldDef<?, ?> field : schema.getFields().values()) {
       String name = field.getName();
       FieldType<?> fieldType = field.getType();
-      if (fieldType == FieldType.EXACT) {
+      if (fieldType == FieldType.EXACT || fieldType == FieldType.KEYWORD) {
         mapping.addExactField(name);
       } else if (fieldType == FieldType.TIMESTAMP) {
         mapping.addTimestamp(name);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index e9194c7..a536b41 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -16,6 +16,9 @@
 
 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,23 +28,19 @@
 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 {
@@ -57,48 +56,49 @@
 
   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 clientBuilder,
       @Assisted Schema<ProjectData> schema) {
     super(cfg, sitePaths, schema, clientBuilder, PROJECTS);
     this.projectCache = projectCache;
+    this.schema = schema;
     this.mapping = new ProjectMapping(schema);
   }
 
   @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, PROJECTS)
+            .add(new UpdateRequest<>(schema, projectState));
+
+    String uri = getURI(PROJECTS, 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), PROJECTS, sortArray);
   }
 
   @Override
-  protected Builder addActions(Builder builder, Project.NameKey nameKey) {
-    return builder.addAction(delete(PROJECTS, nameKey));
+  protected String addActions(Project.NameKey nameKey) {
+    return delete(PROJECTS, nameKey);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index 6905cf4..c8c6345 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 {
@@ -103,7 +94,7 @@
       return intRangeQuery(p);
     } else if (type == FieldType.TIMESTAMP) {
       return timestampQuery(p);
-    } else if (type == FieldType.EXACT) {
+    } else if (type == FieldType.EXACT || type == FieldType.KEYWORD) {
       return exactQuery(p);
     } else if (type == FieldType.PREFIX) {
       return QueryBuilders.matchPhrasePrefixQuery(name, value);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
new file mode 100644
index 0000000..102305b
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
@@ -0,0 +1,142 @@
+// 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;
+
+  @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);
+        }
+      }
+    }
+    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.
+      }
+    }
+  }
+
+  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..7a12080
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/MatchQueryBuilder.java
@@ -0,0 +1,65 @@
+// 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. */
+    PHRASE,
+    /** The text is analyzed and used in a phrase query, with the last term acting as a prefix. */
+    PHRASE_PREFIX
+  }
+
+  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("match");
+    builder.startObject(name);
+
+    builder.field("query", text);
+    if (type != null) {
+      builder.field("type", type.toString().toLowerCase(Locale.ENGLISH));
+    }
+    builder.endObject();
+    builder.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..26fac4c
--- /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.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.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..e72e9fa
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/builders/SearchSourceBuilder.java
@@ -0,0 +1,108 @@
+// 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.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 QuerySourceBuilder querySourceBuilder;
+
+  private int from = -1;
+
+  private int size = -1;
+
+  private List<String> fieldNames;
+
+  /** Constructs a new search source builder. */
+  public SearchSourceBuilder() {}
+
+  /** 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("fields", fieldNames.get(0));
+      } else {
+        builder.startArray("fields");
+        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..c7757b2
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.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.elasticsearch.bulk;
+
+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;
+
+  protected ActionRequest(String action, String id, String index, String type) {
+    this.action = action;
+    this.id = id;
+    this.index = index;
+    this.type = type;
+  }
+
+  @Override
+  protected String getRequest() {
+    JsonObject properties = new JsonObject();
+    properties.addProperty("_id", id);
+    properties.addProperty("_index", index);
+    properties.addProperty("_type", 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..7d549ca
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java
@@ -0,0 +1,22 @@
+// 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;
+
+public class DeleteRequest extends ActionRequest {
+
+  public DeleteRequest(String id, String index, String type) {
+    super("delete", id, index, type);
+  }
+}
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..b131501
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java
@@ -0,0 +1,22 @@
+// 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;
+
+public class IndexRequest extends ActionRequest {
+
+  public IndexRequest(String id, String index, String type) {
+    super("index", id, index, type);
+  }
+}
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/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 4fd30a7..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));
           }
         }
       }
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 d4045dd..4bd3e2e 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -33,11 +33,11 @@
         "//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/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/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/container/HttpLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index d86c85a..6340b22 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;
@@ -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/ldap/LdapLoginServlet.java b/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index 4671475..6370476 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;
@@ -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..9c48832 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -17,10 +17,10 @@
         "//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/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/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index cc22d24..383efd3 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;
@@ -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..728fcd9 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;
@@ -127,12 +128,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 +193,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));
       }
 
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index eb75a97..63ce211 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;
@@ -92,14 +93,13 @@
 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;
@@ -191,7 +191,7 @@
       try {
         filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
       } catch (RuntimeException e) {
-        log.warn("Plugin {} cannot load GuiceFilter", name, e);
+        logger.atWarning().withCause(e).log("Plugin %s cannot load GuiceFilter", name);
         return null;
       }
 
@@ -199,7 +199,7 @@
         ServletContext ctx = PluginServletContext.create(plugin, wrapper.getFullPath(name));
         filter.init(new WrappedFilterConfig(ctx));
       } catch (ServletException e) {
-        log.warn("Plugin {} failed to initialize HTTP", name, e);
+        logger.atWarning().withCause(e).log("Plugin %s failed to initialize HTTP", name);
         return null;
       }
 
@@ -423,12 +423,9 @@
               && (name.endsWith(".md") || name.endsWith(".html"))
               && size.isPresent()) {
             if (size.get() <= 0 || size.get() > SMALL_RESOURCE) {
-              log.warn(
-                  "Plugin {}: {} omitted from document index. Size {} out of range (0,{}).",
-                  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;
@@ -450,10 +447,9 @@
         if (about == null) {
           about = entry;
         } else {
-          log.warn(
-              "Plugin {}: Multiple 'about' documents found; using {}",
-              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);
@@ -731,7 +727,8 @@
         }
         return def;
       } catch (IOException e) {
-        log.warn("Error getting {} for plugin {}, 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 a8a8502..0ee22fa 100644
--- a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -18,6 +18,7 @@
 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;
@@ -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("Plugin {} 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("Plugin {} 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 8f64d9f..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("[plugin {}] {}", 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..92a5aaa 100644
--- a/java/com/google/gerrit/httpd/raw/BazelBuild.java
+++ b/java/com/google/gerrit/httpd/raw/BazelBuild.java
@@ -19,6 +19,7 @@
 
 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;
@@ -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..74868d7 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;
@@ -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/ResourceServlet.java b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 3ec6bdb..035653d 100644
--- a/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -30,6 +30,7 @@
 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;
@@ -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("Path doesn't exist {}", name);
+        logger.atWarning().log("Path doesn't exist %s", name);
         return null;
       }
       return cache.get(p, newLoader(p));
     } catch (ExecutionException | IOException e) {
-      log.warn("Cannot load static resource {}", name, e);
+      logger.atWarning().withCause(e).log("Cannot load static resource %s", name);
       return null;
     }
   }
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/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 913128e..4c9a035 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;
@@ -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";
@@ -1192,7 +1192,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/FieldDef.java b/java/com/google/gerrit/index/FieldDef.java
index b1ffac1..1d0a594 100644
--- a/java/com/google/gerrit/index/FieldDef.java
+++ b/java/com/google/gerrit/index/FieldDef.java
@@ -34,6 +34,10 @@
     return new FieldDef.Builder<>(FieldType.EXACT, name);
   }
 
+  public static FieldDef.Builder<String> keyword(String name) {
+    return new FieldDef.Builder<>(FieldType.KEYWORD, name);
+  }
+
   public static FieldDef.Builder<String> fullText(String name) {
     return new FieldDef.Builder<>(FieldType.FULL_TEXT, name);
   }
diff --git a/java/com/google/gerrit/index/FieldType.java b/java/com/google/gerrit/index/FieldType.java
index 0db0284..376c071 100644
--- a/java/com/google/gerrit/index/FieldType.java
+++ b/java/com/google/gerrit/index/FieldType.java
@@ -33,6 +33,9 @@
   /** A string field searched using exact-match semantics. */
   public static final FieldType<String> EXACT = new FieldType<>("EXACT");
 
+  /** A Keyword field searched using non-analyzed-match semantics. */
+  public static final FieldType<String> KEYWORD = new FieldType<>("KEYWORD");
+
   /** A string field searched using prefix. */
   public static final FieldType<String> PREFIX = new FieldType<>("PREFIX");
 
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index d20aed1..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("error getting field {} of {}", 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..57afffd 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");
     }
   }
 
@@ -332,7 +330,7 @@
       for (Object value : values.getValues()) {
         doc.add(new LongField(name, ((Timestamp) value).getTime(), store));
       }
-    } else if (type == FieldType.EXACT || type == FieldType.PREFIX) {
+    } else if (type == FieldType.KEYWORD || type == FieldType.EXACT || type == FieldType.PREFIX) {
       for (Object value : values.getValues()) {
         doc.add(new StringField(name, (String) value, store));
       }
@@ -355,7 +353,10 @@
     for (IndexableField field : doc.getFields()) {
       checkArgument(allFields.containsKey(field.name()), "Unrecognized field " + field.name());
       FieldType<?> type = allFields.get(field.name()).getType();
-      if (type == FieldType.EXACT || type == FieldType.FULL_TEXT || type == FieldType.PREFIX) {
+      if (type == FieldType.EXACT
+          || type == FieldType.FULL_TEXT
+          || type == FieldType.PREFIX
+          || type == FieldType.KEYWORD) {
         rawFields.put(field.name(), field.stringValue());
       } else if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
         rawFields.put(field.name(), field.numericValue().intValue());
@@ -450,7 +451,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 +527,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..c7fc880 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 || type == FieldType.KEYWORD) {
       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/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..c83f8af 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -51,11 +51,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 53e43a2..3d5cde4 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;
@@ -127,12 +128,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;
@@ -169,20 +168,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)
@@ -249,7 +246,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());
           }
         });
 
@@ -269,17 +266,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);
         }
       }
 
@@ -299,7 +296,7 @@
       }
       return 0;
     } catch (Throwable err) {
-      log.error("Unable to start daemon", err);
+      logger.atSevere().withCause(err).log("Unable to start daemon");
       return 1;
     }
   }
@@ -366,7 +363,7 @@
       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();
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 1a22162..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("File {} doesn't exist", newSecureStorePath.toAbsolutePath());
+      logger.atSevere().log("File %s doesn't exist", newSecureStorePath.toAbsolutePath());
       return -1;
     }
 
@@ -76,18 +74,18 @@
     String currentSecureStoreName = getCurrentSecureStoreClassName(sitePaths);
 
     if (currentSecureStoreName.equals(newSecureStore)) {
-      log.error(
-          "Old and new SecureStore implementation names are the same. Migration will not work");
+      logger.atSevere().log(
+          "Old and new SecureStore implementation names "
+              + "are the same. Migration will not work");
       return -1;
     }
 
     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);
@@ -103,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) {
@@ -122,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);
@@ -197,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..6dc63658 100644
--- a/java/com/google/gerrit/pgm/http/jetty/BUILD
+++ b/java/com/google/gerrit/pgm/http/jetty/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gwtexpui/server",
         "//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 2fbbb97..9347171 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 
 import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
 import com.google.gwtexpui.server.CacheHeaders;
 import java.io.IOException;
 import javax.servlet.ServletOutputStream;
@@ -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("Error in {} {}", 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 45d2a5d..057496f 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -65,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);
   }
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/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/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 56b1724..db762dd 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -374,31 +374,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 {
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/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/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..0d8d7c1 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();
     }
   }
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index dacbe37..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("{} 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("{} 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 56c9f76..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("Cannot look up group {} 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("Cannot look up group {} 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/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 1f77773..c23dd99 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -25,6 +25,7 @@
 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;
@@ -36,13 +37,11 @@
 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();
 
   private final LoadingCache<ObjectId, AllExternalIds> extIdsByAccount;
   private final ExternalIdReader externalIdReader;
@@ -154,7 +153,7 @@
       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();
     }
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 fe57374..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;
@@ -57,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";
@@ -155,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");
     }
   }
 
@@ -198,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();
@@ -345,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);
@@ -476,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 bb3d6f8..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("Cannot lookup group {} 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 39c8a2f..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("Cannot lookup membershipsOf {} 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/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 4aeac962..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("Cannot lookup account {} 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/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index f8d0105..96eba1c 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -9,8 +9,8 @@
         "//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/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 451540d..a1cc1c2 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) {
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 7db1a35..1c87692 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -21,6 +21,7 @@
 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.TimeUtil;
 import com.google.gerrit.server.cache.CacheSerializer;
@@ -46,8 +47,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 +70,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");
@@ -347,11 +346,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 +357,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 +402,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;
@@ -475,7 +473,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 +486,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 +515,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 +529,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 +558,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 +580,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 +651,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 +663,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/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 a01ad9e..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("Auto-Abandoned {} of {} 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 f3ab847..24685af 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -21,6 +21,7 @@
 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;
@@ -58,11 +59,9 @@
 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";
 
@@ -108,8 +107,8 @@
         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;
       }
     }
@@ -345,7 +344,8 @@
       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;
     }
   }
@@ -397,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;
@@ -427,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 b57be15..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;
@@ -46,12 +47,10 @@
 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";
 
@@ -213,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/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/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 bc9d190..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("Cannot get size information for {}.", 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 5bc4bb4..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(
-          "Error in event listener {} for event {}",
-          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("Error in event listener {}", 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 997907e..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,11 +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);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
   private final GarbageCollectionQueue gcQueue;
@@ -118,7 +117,7 @@
       try {
         l.onGarbageCollected(event);
       } catch (RuntimeException e) {
-        log.warn("Failure in GarbageCollectorListener", e);
+        logger.atWarning().withCause(e).log("Failure in GarbageCollectorListener");
       }
     }
   }
@@ -139,7 +138,7 @@
       }
       b.append(s);
     }
-    log.info(b.toString());
+    logger.atInfo().log(b.toString());
   }
 
   private static void logGcConfiguration(
@@ -179,7 +178,7 @@
     print(writer, "failed.\n\n");
     StringBuilder b = new StringBuilder();
     b.append("[").append(projectName.get()).append("]");
-    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 8796fdf..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;
@@ -40,13 +41,17 @@
 
   @Override
   public void stop() {
-    LogManager.getLogger(GarbageCollection.class).removeAllAppenders();
-    LogManager.getLogger(GarbageCollectionRunner.class).removeAllAppenders();
+    getLogger(GarbageCollection.class).removeAllAppenders();
+    getLogger(GarbageCollectionRunner.class).removeAllAppenders();
   }
 
   private static void initLogSystem(Path logdir, boolean rotate) {
-    initGcLogger(logdir, rotate, LogManager.getLogger(GarbageCollection.class));
-    initGcLogger(logdir, rotate, LogManager.getLogger(GarbageCollectionRunner.class));
+    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) {
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
index 054e56a..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 log = LoggerFactory.getLogger(GarbageCollectionRunner.class);
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static class Lifecycle implements LifecycleListener {
     private final WorkQueue queue;
@@ -61,7 +60,7 @@
 
   @Override
   public void run() {
-    log.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 2c035d3..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("Defaulting core.streamFileThreshold to {}", desc);
+        logger.atInfo().log("Defaulting core.streamFileThreshold to %s", desc);
         cfg.setStreamFileThreshold(limit);
       }
       cfg.install();
@@ -193,7 +192,8 @@
       //
       File metaConfigLog = new File(db.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
       if (!metaConfigLog.getParentFile().mkdirs() || !metaConfigLog.createNewFile()) {
-        log.error("Failed to create ref log for {} in repository {}", RefNames.REFS_CONFIG, name);
+        logger.atSevere().log(
+            "Failed to create ref log for %s in repository %s", RefNames.REFS_CONFIG, name);
       }
 
       return db;
@@ -246,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());
     }
   }
 
@@ -286,7 +287,7 @@
 
     @Override
     public FileVisitResult visitFileFailed(Path file, IOException e) {
-      log.warn(e.getMessage());
+      logger.atWarning().log(e.getMessage());
       return FileVisitResult.CONTINUE;
     }
 
@@ -301,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 74f986e..8140738 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.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;
@@ -44,12 +45,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;
 
@@ -75,12 +76,11 @@
     }
   }
 
-  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());
         }
       };
 
@@ -106,12 +106,13 @@
   }
 
   /** Create a new executor queue. */
-  public ScheduledExecutorService createQueue(int poolsize, String prefix) {
-    return createQueue(poolsize, prefix, Thread.NORM_PRIORITY);
+  public ScheduledExecutorService createQueue(int poolsize, String queueName) {
+    return createQueue(poolsize, queueName, Thread.NORM_PRIORITY);
   }
 
-  public ScheduledThreadPoolExecutor createQueue(int poolsize, String prefix, int threadPriority) {
-    Executor executor = new Executor(poolsize, prefix);
+  public ScheduledThreadPoolExecutor createQueue(
+      int poolsize, String queueName, int threadPriority) {
+    Executor executor = new Executor(poolsize, queueName);
     executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
     executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(true);
     queues.add(executor);
@@ -201,7 +202,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() {
@@ -211,7 +212,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;
             }
@@ -223,7 +224,7 @@
               0.75f, // load factor
               corePoolSize + 4 // concurrency level
               );
-      queueName = prefix;
+      this.queueName = queueName;
     }
 
     @Override
@@ -478,7 +479,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..78432bc 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(
@@ -1255,11 +1254,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 +1269,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 +1293,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 +1316,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 +1330,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 +1346,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 +1367,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");
@@ -1738,7 +1728,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;
     }
   }
@@ -2707,20 +2697,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");
           }
         }
       }
@@ -3115,19 +3106,13 @@
   }
 
   private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(receiveId + msg, args);
+    if (logger.atFine().isEnabled()) {
+      logger.atFine().logVarargs(receiveId + msg, args);
     }
   }
 
   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 +3120,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..37c31f5 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;
@@ -78,10 +79,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 +98,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;
@@ -486,7 +485,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 +515,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 +600,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/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 6c31369..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("error checking staleness of {} in {}", 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/group/GroupField.java b/java/com/google/gerrit/server/index/group/GroupField.java
index 29e3867..779dc6b 100644
--- a/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/java/com/google/gerrit/server/index/group/GroupField.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.fullText;
 import static com.google.gerrit.index.FieldDef.integer;
+import static com.google.gerrit.index.FieldDef.keyword;
 import static com.google.gerrit.index.FieldDef.prefix;
 import static com.google.gerrit.index.FieldDef.storedOnly;
 import static com.google.gerrit.index.FieldDef.timestamp;
@@ -40,11 +41,11 @@
 
   /** Group UUID. */
   public static final FieldDef<InternalGroup, String> UUID =
-      exact("uuid").stored().build(g -> g.getGroupUUID().get());
+      keyword("uuid").stored().build(g -> g.getGroupUUID().get());
 
   /** Group owner UUID. */
   public static final FieldDef<InternalGroup, String> OWNER_UUID =
-      exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get());
+      keyword("owner_uuid").build(g -> g.getOwnerGroupUUID().get());
 
   /** Timestamp indicating when this group was created. */
   public static final FieldDef<InternalGroup, Timestamp> CREATED_ON =
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 c558d63..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,11 +144,9 @@
       throws OrmException, UpdateException, RestApiException, IOException {
     for (DynamicMap.Entry<MailFilter> filter : mailFilters) {
       if (!filter.getProvider().get().shouldProcessMessage(message)) {
-        log.warn(
-            "Message {} filtered by plugin {} {}. 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;
       }
     }
@@ -157,10 +154,9 @@
     MailMetadata metadata = MailHeaderParser.parse(message);
 
     if (!metadata.hasRequiredFields()) {
-      log.error(
-          "Message {} is missing required metadata, have {}. 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;
     }
@@ -168,11 +164,11 @@
     Set<Account.Id> accountIds = emails.getAccountFor(metadata.author);
 
     if (accountIds.size() != 1) {
-      log.error(
-          "Address {} could not be matched to a unique account. It was matched to {}."
+      logger.atSevere().log(
+          "Address %s could not be matched to a unique account. It was matched to %s."
               + " Will delete message.",
-          metadata.author,
-          accountIds);
+          metadata.author, accountIds);
+
       // We don't want to send an email if no accounts are linked to it.
       if (accountIds.size() > 1) {
         sendRejectionEmail(message, InboundEmailRejectionSender.Error.UNKNOWN_ACCOUNT);
@@ -182,11 +178,11 @@
     Account.Id accountId = accountIds.iterator().next();
     Optional<AccountState> accountState = accountCache.get(accountId);
     if (!accountState.isPresent()) {
-      log.warn("Mail: Account {} 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;
     }
@@ -200,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");
     }
   }
 
@@ -211,19 +207,18 @@
       List<ChangeData> changeDataList =
           queryProvider.get().byLegacyChangeId(new Change.Id(metadata.changeNumber));
       if (changeDataList.size() != 1) {
-        log.error(
-            "Message {} references unique change {}, but there are {} 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("Message {} 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
@@ -246,7 +241,8 @@
       }
 
       if (parsedComments.isEmpty()) {
-        log.warn("Could not parse any comments from {}. 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 bbff29f..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,12 +226,9 @@
           try {
             currentGroup.fileData = new PatchFile(repo, patchList, c.key.filename);
           } catch (IOException e) {
-            log.warn(
-                "Cannot load {} from {} in {}",
-                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;
           }
         }
@@ -322,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();
     }
   }
@@ -540,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("Failed to read file on side {}", 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("Failed to get line number of file on side {}", 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("Side {} 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 99868a4..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("Multiple changes found for {}", 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 06d940e..0bf2108 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -346,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/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 cf54bf4..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("plugin {}: including {}", name, classes);
+          logger.atInfo().log("plugin %s: including %s", name, classes);
           urls.add(classes.toUri().toURL());
         }
       }
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 73f2cb2..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;
@@ -62,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));
@@ -168,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(
-          "Plugin provides its own name: <{}>, use it instead of the input name: <{}>",
-          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);
@@ -180,7 +178,7 @@
       Plugin active = running.get(name);
       if (active != null) {
         fileName = active.getSrcFile().getFileName().toString();
-        log.info("Replacing plugin {}", active.getName());
+        logger.atInfo().log("Replacing plugin %s", active.getName());
         Path old = pluginsDir.resolve(".last_" + fileName);
         Files.deleteIfExists(old);
         Files.move(active.getSrcFile(), old);
@@ -191,7 +189,7 @@
       try {
         Plugin plugin = runPlugin(name, dst, active);
         if (active == null) {
-          log.info("Installed plugin {}", plugin.getName());
+          logger.atInfo().log("Installed plugin %s", plugin.getName());
         }
       } catch (PluginInstallException e) {
         Files.deleteIfExists(dst);
@@ -207,7 +205,7 @@
   private synchronized void unloadPlugin(Plugin plugin) {
     persistentCacheFactory.onStop(plugin.getName());
     String name = plugin.getName();
-    log.info("Unloading plugin {}, version {}", name, plugin.getVersion());
+    logger.atInfo().log("Unloading plugin %s, version %s", name, plugin.getVersion());
     plugin.stop(env);
     env.onStopPlugin(plugin);
     running.remove(name);
@@ -217,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;
     }
 
@@ -228,13 +227,13 @@
           continue;
         }
 
-        log.info("Disabling plugin {}", 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.
@@ -248,7 +247,8 @@
           disabled.put(name, offPlugin);
         } catch (Throwable e) {
           // This shouldn't happen, as the plugin was loaded earlier.
-          log.warn("Cannot load disabled plugin {}", active.getName(), e.getCause());
+          logger.atWarning().withCause(e.getCause()).log(
+              "Cannot load disabled plugin %s", active.getName());
         }
       }
       cleanInBackground();
@@ -257,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;
     }
 
@@ -268,7 +269,7 @@
           continue;
         }
 
-        log.info("Enabling plugin {}", name);
+        logger.atInfo().log("Enabling plugin %s", name);
         String n = off.getSrcFile().toFile().getName();
         if (n.endsWith(".disabled")) {
           n = n.substring(0, n.lastIndexOf('.'));
@@ -277,7 +278,7 @@
         try {
           Files.move(off.getSrcFile(), on);
         } catch (IOException e) {
-          log.error("Failed to move plugin {} into place", name, e);
+          logger.atSevere().withCause(e).log("Failed to move plugin %s into place", name);
           continue;
         }
         disabled.remove(name);
@@ -297,16 +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(
-              "Failed to remove stale plugin file {}: {}", 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());
     }
   }
 
@@ -315,14 +316,14 @@
     removeStalePluginFiles();
     Path absolutePath = pluginsDir.toAbsolutePath();
     if (!Files.exists(absolutePath)) {
-      log.info("{} does not exist; creating", absolutePath);
+      logger.atInfo().log("%s does not exist; creating", absolutePath);
       try {
         Files.createDirectories(absolutePath);
       } catch (IOException e) {
-        log.error("Failed to create {}: {}", 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;
@@ -371,11 +372,12 @@
       for (Plugin active : reload) {
         String name = active.getName();
         try {
-          log.info("Reloading plugin {}", name);
+          logger.atInfo().log("Reloading plugin %s", name);
           Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
-          log.info("Reloaded plugin {}, version {}", newPlugin.getName(), newPlugin.getVersion());
+          logger.atInfo().log(
+              "Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion());
         } catch (PluginInstallException e) {
-          log.warn("Cannot reload plugin {}", 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,20 +416,20 @@
       }
 
       if (active != null) {
-        log.info("Reloading plugin {}", active.getName());
+        logger.atInfo().log("Reloading plugin %s", active.getName());
       }
 
       try {
         Plugin loadedPlugin = runPlugin(name, path, active);
         if (!loadedPlugin.isDisabled()) {
-          log.info(
-              "{} plugin {}, version {}",
+          logger.atInfo().log(
+              "%s plugin %s, version %s",
               active == null ? "Loaded" : "Reloaded",
               loadedPlugin.getName(),
               loadedPlugin.getVersion());
         }
       } catch (PluginInstallException e) {
-        log.warn("Cannot load plugin {}", name, e.getCause());
+        logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
       }
     }
 
@@ -660,20 +663,18 @@
       Collection<Path> elementsToRemove = new ArrayList<>();
       Collection<Path> elementsToAdd = new ArrayList<>();
       for (Path loser : Iterables.skip(enabled, 1)) {
-        log.warn(
-            "Plugin <{}> was disabled, because"
-                + " another plugin <{}>"
-                + " with the same name <{}> 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);
@@ -686,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/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 3e6589c..6ae00e1 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -16,6 +16,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.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
@@ -33,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;
@@ -160,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/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/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/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/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 e2842ee..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;
@@ -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/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 7ff7860..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);
@@ -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/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/GetOAuthToken.java b/java/com/google/gerrit/server/restapi/account/GetOAuthToken.java
index 93330a0..eece080 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> {
+  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;
@@ -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/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/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/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index c0890d7..a4b340b 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");
     }
   }
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/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/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 51a4090..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 =
@@ -293,7 +292,7 @@
             + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", "));
       }
     } 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;
@@ -314,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);
     }
 
@@ -325,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/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/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/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/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/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index 130377a..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("Failed to update config of project {}.", 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(
-                        "The type '{}' of parameter '{}' 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..b7eb68f 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);
@@ -105,8 +104,8 @@
   }
 
   private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(submissionId + msg, args);
+    if (logger.atFine().isEnabled()) {
+      logger.atFine().logVarargs(submissionId + msg, args);
     }
   }
 }
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..b5b11f2 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);
       }
     }
@@ -938,31 +937,21 @@
   }
 
   private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(submissionId + msg, args);
+    if (logger.atFine().isEnabled()) {
+      logger.atFine().logVarargs(submissionId + msg, args);
     }
   }
 
   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..48d8a63 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -63,11 +64,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;
@@ -290,7 +289,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.
@@ -532,7 +531,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 +548,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(
@@ -602,25 +601,17 @@
   }
 
   protected final void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(this.args.submissionId + msg, args);
+    if (logger.atFine().isEnabled()) {
+      logger.atFine().logVarargs(this.args.submissionId + msg, args);
     }
   }
 
   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..e33aa95 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -19,6 +19,7 @@
 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.data.SubscribeSection;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -70,10 +71,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 +128,6 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
-
   private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
   private final ProjectCache projectCache;
@@ -679,8 +677,8 @@
   }
 
   private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(orm.getSubmissionId() + msg, args);
+    if (logger.atFine().isEnabled()) {
+      logger.atFine().logVarargs(orm.getSubmissionId() + " " + msg, args);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/submit/TestHelperOp.java b/java/com/google/gerrit/server/submit/TestHelperOp.java
index b3a82de..d9304a0 100644
--- a/java/com/google/gerrit/server/submit/TestHelperOp.java
+++ b/java/com/google/gerrit/server/submit/TestHelperOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -22,11 +23,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;
@@ -51,8 +50,8 @@
   }
 
   private void logDebug(String msg, Object... args) {
-    if (log.isDebugEnabled()) {
-      log.debug(submissionId + msg, args);
+    if (logger.atFine().isEnabled()) {
+      logger.atFine().logVarargs(submissionId + msg, args);
     }
   }
 }
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..e665b7a 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,8 +385,8 @@
   }
 
   protected void logDebug(String msg, Throwable t) {
-    if (requestId != null && log.isDebugEnabled()) {
-      log.debug(requestId + msg, t);
+    if (requestId != null && logger.atFine().isEnabled()) {
+      logger.atFine().withCause(t).log(requestId + "%s", msg);
     }
   }
 
@@ -395,8 +394,8 @@
     // 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().isEnabled()) {
+      logger.atFine().logVarargs(requestId + msg, args);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 9cdb006..97db475 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(
@@ -579,7 +578,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.
@@ -732,7 +731,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,14 +826,14 @@
     }
 
     private void logDebug(String msg, Throwable t) {
-      if (log.isDebugEnabled()) {
-        ReviewDbBatchUpdate.this.logDebug("[" + taskId + "]" + msg, t);
+      if (logger.atFine().isEnabled()) {
+        ReviewDbBatchUpdate.this.logDebug("[" + taskId + "] " + msg, t);
       }
     }
 
     private void logDebug(String msg, Object... args) {
-      if (log.isDebugEnabled()) {
-        ReviewDbBatchUpdate.this.logDebug("[" + taskId + "]" + msg, args);
+      if (logger.atFine().isEnabled()) {
+        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/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 4cd7487..712b664 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;
@@ -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 1c46ec8..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;
@@ -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");
       }
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/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/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/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/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..f17165e 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -916,9 +916,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 +930,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 +954,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/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..ede0397
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -0,0 +1,288 @@
+// 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.Ignore;
+import org.junit.Test;
+
+@NoHttpd
+@Ignore
+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..49c34dd
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -0,0 +1,22 @@
+// 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 com.google.gerrit.acceptance.NoHttpd;
+import org.junit.Ignore;
+
+@NoHttpd
+@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 41640a9..e4a2805 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -14,273 +14,7 @@
 
 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());
-      // 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)));
-  }
-}
+public class ReindexIT extends AbstractReindexTests {}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index e510e25..3e9b90c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -273,7 +273,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 3360cf4..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,10 +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",
     ],
 )
@@ -34,6 +39,7 @@
     size = "large",
     srcs = [src],
     tags = [
+        "docker",
         "elastic",
     ],
     deps = [
@@ -44,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..a7a23c3
--- /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: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 3358037..4f0f8b0 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
@@ -21,7 +21,6 @@
 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.AfterClass;
 import org.junit.BeforeClass;
@@ -33,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();
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
index dbc68df..2aef875 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
@@ -22,7 +22,6 @@
 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.AfterClass;
@@ -36,22 +35,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();
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
index fe0f7dd..f13c491 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
@@ -21,7 +21,6 @@
 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.AfterClass;
 import org.junit.BeforeClass;
@@ -33,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();
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
index 285c013..dd04010 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
@@ -21,7 +21,6 @@
 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.AfterClass;
 import org.junit.BeforeClass;
@@ -33,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();
     }
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 00d1c7a..ca52e2a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -14,90 +14,31 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.base.Strings;
-import com.google.common.io.Files;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.server.index.IndexModule.IndexType;
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.util.Collection;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
 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;
 
 public final class ElasticTestUtils {
   public static class ElasticNodeInfo {
-    public final Node node;
-    public final String port;
-    public final File elasticDir;
+    public final int port;
 
-    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);
-  }
-
-  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 class NodeInfo {
-    String httpAddress;
-  }
-
-  static class Info {
-    Map<String, NodeInfo> nodes;
+    config.setString("index", null, "maxLimit", "10000");
   }
 
   public static void createAllIndexes(Injector injector) throws IOException {
@@ -108,28 +49,6 @@
     }
   }
 
-  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");
-    }
-    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() {
     // hide default constructor
   }
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/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/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/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/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/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/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/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..7252f8c 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -41,6 +41,7 @@
     "//lib/commons:lang",
     "//lib/commons:lang3",
     "//lib/dropwizard:dropwizard-core",
+    "//lib/flogger:api",
     "//lib/guice:guice",
     "//lib/guice:guice-assistedinject",
     "//lib/guice:guice-servlet",
diff --git a/plugins/replication b/plugins/replication
index 5e91925..24bcb95 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 5e91925cfd391898e8e33fd149b9e1a115dafee4
+Subproject commit 24bcb95e9cea11a7e7ea93e39e2956c523d28d91
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 45003c4..ece97db 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 45003c4e290cd29a8695db438ca30302fa0683c7
+Subproject commit ece97dbd5776cf4366c2ff570fdb3e0bf933651c
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 6913f98..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
@@ -63,7 +63,7 @@
         text-align: center;
       }
       iron-icon {
-        color: var(--default-button-text-color);
+        color: inherit;
         height: 1.2rem;
         margin-right: .2rem;
         width: 1.2rem;
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 5f0a620..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
@@ -58,6 +58,7 @@
   };
 
   const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
+  const SEND_REPLY_TIMING_LABEL = 'SendReply';
 
   Polymer({
     is: 'gr-change-view',
@@ -552,7 +553,9 @@
 
     _handleReplySent(e) {
       this.$.replyOverlay.close();
-      this._reload();
+      this._reload().then(() => {
+        this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
+      });
     },
 
     _handleReplyCancel(e) {
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 eb24d8e..2582d3c 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,8 +26,10 @@
   const SIZE_BAR_GAP_WIDTH = 1;
   const SIZE_BAR_MIN_WIDTH = 1.5;
 
-  const RENDER_TIME = 'FileListRenderTime';
-  const RENDER_TIME_AVERAGE = 'FileListRenderTimePerFile';
+  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',
@@ -338,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) {
@@ -819,7 +820,7 @@
       // 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 =
@@ -934,13 +935,12 @@
           })
           .reduce((acc, paths) => { return acc.concat(paths); }, []);
 
-      const timerName = 'Expand ' + newPaths.length + ' diffs';
-      this.$.reporting.time(timerName);
+      this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
 
       // 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._renderInOrder(newPaths, this.diffs, newPaths.length);
       this._updateDiffCursor();
       this.$.diffCursor.handleDiffUpdate();
     },
@@ -960,11 +960,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 => {
@@ -988,7 +986,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();
         });
       });
@@ -1214,8 +1213,8 @@
     _reportRenderedRow(index) {
       if (index === this._shownFiles.length - 1) {
         this.async(() => {
-          this.$.reporting.timeEndWithAverage(RENDER_TIME, RENDER_TIME_AVERAGE,
-              this._reportinShownFilesIncrement);
+          this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
+              RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
         }, 1);
       }
       return '';
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 df18fda..feff01d 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -151,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);
     },
@@ -177,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);
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 f0440d2..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
@@ -202,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(
@@ -215,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);
       });
@@ -237,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/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 6a2bfe0..efcefe2 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -206,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)]]">
           |
@@ -220,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">
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 3b66ae8..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,6 +130,7 @@
       });
       this.$.restAPI.getVersion().then(version => {
         this._version = version;
+        this._logWelcome();
       });
 
       if (window.localStorage.getItem('dark-theme')) {
@@ -191,7 +197,10 @@
         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();
     },
@@ -272,7 +281,7 @@
 
     _handleRegistrationDialogClose(e) {
       this.params.justRegistered = false;
-      this.$.registration.close();
+      this.$.registrationOverlay.close();
     },
 
     _computeShadowClass(isShadowDom) {
@@ -310,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-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index c19e3d5..9102bdb 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -69,7 +69,7 @@
       <!-- 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"><gcpath 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>
+      <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 -->
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/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..90c1bd2 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
@@ -17,6 +17,8 @@
 
 'use strict';
 
+const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/;
+
 function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) {
   this.linkConfig = linkConfig;
   this.callback = callback;
@@ -138,7 +140,11 @@
     text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
   }
 
-  if (href) {
+  // 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 {
     this.parseLinks(text, this.linkConfig);
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 546c75e..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",
 ]
 
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