Fifth cl for verbose workspaces (ability to log certain potentially non-hermetic events that happen as part of repository rules).

* Allow to specify log file rather than dumping to INFO.
* Include a parser that is able to convert that binary file to text, optionally
filtering out specific rules.

In the future:
- Log levels, full or alerts only

RELNOTES: None
PiperOrigin-RevId: 210620591
diff --git a/src/BUILD b/src/BUILD
index 50ec65e..c717547 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -350,6 +350,7 @@
         "//src/test/shell:srcs",
         "//src/tools/android/java/com/google/devtools/build/android:srcs",
         "//src/tools/execlog:srcs",
+        "//src/tools/workspacelog:srcs",
         "//src/tools/launcher:srcs",
         "//src/tools/package_printer/java/com/google/devtools/build/packageprinter:srcs",
         "//src/tools/skylark/java/com/google/devtools/skylark/skylint:srcs",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD
index ad9343c..59f1202 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD
@@ -53,7 +53,10 @@
         ":debugging-options",
         ":workspace-rule-event",
         "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:exitcode-external",
+        "//src/main/java/com/google/devtools/build/lib:io",
         "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/common/options",
         "//third_party:guava",
     ],
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java
index 2761b61..4bed8b0 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java
@@ -22,10 +22,12 @@
 /** Options for debugging and verbosity tools. */
 public final class DebuggingOptions extends OptionsBase {
   @Option(
-      name = "experimental_workspace_rules_logging",
+      name = "experimental_workspace_rules_log_file",
       defaultValue = "null",
+      category = "verbosity",
       documentationCategory = OptionDocumentationCategory.LOGGING,
       effectTags = {OptionEffectTag.UNKNOWN},
-      help = "Log certain Workspace Rules events")
-  public String workspaceRulesLogging;
+      help =
+          "Log certain Workspace Rules events into this file as delimited WorkspaceEvent protos.")
+  public String workspaceRulesLogFile;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java b/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java
index bd5406d..9efbcf6 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java
@@ -20,16 +20,20 @@
 import com.google.devtools.build.lib.events.Reporter;
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
 import com.google.devtools.common.options.OptionsBase;
+import java.io.IOException;
 
 /** A module for logging workspace rule events */
 public final class WorkspaceRuleModule extends BlazeModule {
   private Reporter reporter;
   private EventBus eventBus;
+  private AsynchronousFileOutputStream outFileStream;
 
   @Override
   public void beforeCommand(CommandEnvironment env) {
-
     reporter = env.getReporter();
     eventBus = env.getEventBus();
 
@@ -38,18 +42,43 @@
       return;
     }
 
-    if (env.getOptions().getOptions(DebuggingOptions.class).workspaceRulesLogging != null) {
+    String logFile = env.getOptions().getOptions(DebuggingOptions.class).workspaceRulesLogFile;
+    if (logFile != null && !logFile.isEmpty()) {
+      try {
+        outFileStream = new AsynchronousFileOutputStream(logFile);
+      } catch (IOException e) {
+        env.getReporter().handle(Event.error(e.getMessage()));
+        env.getBlazeModuleEnvironment()
+            .exit(
+                new AbruptExitException(
+                    "Error initializing workspace rule log file.", ExitCode.COMMAND_LINE_ERROR));
+      }
       eventBus.register(this);
     }
   }
 
   @Override
+  public void afterCommand() {
+    if (outFileStream != null) {
+      try {
+        outFileStream.close();
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      } finally {
+        outFileStream = null;
+      }
+    }
+  }
+
+  @Override
   public Iterable<Class<? extends OptionsBase>> getCommonCommandOptions() {
     return ImmutableList.<Class<? extends OptionsBase>>of(DebuggingOptions.class);
   }
 
   @Subscribe
   public void workspaceRuleEventReceived(WorkspaceRuleEvent event) {
-    reporter.handle(Event.info(event.logMessage()));
+    if (outFileStream != null) {
+      outFileStream.write(event.getLogEvent());
+    }
   }
 }
diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD
index 671a9c2..17fef97 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -679,6 +679,9 @@
 sh_test(
     name = "bazel_workspaces_test",
     srcs = ["bazel_workspaces_test.sh"],
-    data = [":test-deps"],
+    data = [
+        ":test-deps",
+        "//src/tools/workspacelog:parser",
+    ],
     tags = ["no_windows"],
 )
diff --git a/src/test/shell/bazel/bazel_workspaces_test.sh b/src/test/shell/bazel/bazel_workspaces_test.sh
index 8bb03be..efc1c6d 100755
--- a/src/test/shell/bazel/bazel_workspaces_test.sh
+++ b/src/test/shell/bazel/bazel_workspaces_test.sh
@@ -23,7 +23,9 @@
 source "${CURRENT_DIR}/remote_helpers.sh" \
   || { echo "remote_helpers.sh not found!" >&2; exit 1; }
 
-function test_execute() {
+# Sets up a workspace with the given commands inserted into the repository rule
+# that will be executed when doing bazel build //:test
+function set_workspace_command() {
   create_new_workspace
   cat > BUILD <<'EOF'
 genrule(
@@ -35,7 +37,7 @@
 EOF
   cat >> repos.bzl <<EOF
 def _executeMe(repository_ctx):
-  repository_ctx.execute(["echo", "testing!"])
+  $1
   build_contents = "package(default_visibility = ['//visibility:public'])\n\n"
   build_contents += "exports_files([\"t.txt\"])\n"
   repository_ctx.file("BUILD", build_contents, False)
@@ -50,24 +52,42 @@
 load("//:repos.bzl", "ex_repo")
 ex_repo(name = "repo")
 EOF
+}
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test"
-  executes=`grep "location: .*repos.bzl:2:3" $TEST_log | wc -l`
-  if [ "$executes" -ne "1" ]
-  then
-    fail "Expected exactly 1 occurrence of the given command, got $executes"
-  fi
+function build_and_process_log() {
+  bazel build //:test --experimental_workspace_rules_log_file=output 2>&1 >> $TEST_log || fail "could not build //:test"
+  ${BAZEL_RUNFILES}/src/tools/workspacelog/parser --log_path=output > output.log.txt "$@" || fail "error parsing output"
+}
 
-  # Cached executions are not replayed
-  bazel build //:test --experimental_workspace_rules_logging=yes &> output || fail "could not build //:test"
-  cat output >> $TEST_log
-  executes=`grep "location: .*repos.bzl:2:3" output | wc -l`
-  if [ "$executes" -ne "0" ]
+function ensure_contains_exactly() {
+  num=`grep "${1}" output.log.txt | wc -l`
+  if [ "$num" -ne $2 ]
   then
-    fail "Expected exactly 0 occurrence of the given command, got $executes"
+    fail "Expected exactly $2 occurences of $1, got $num: " `cat output.log.txt`
   fi
 }
 
+function ensure_contains_atleast() {
+  num=`grep "${1}" output.log.txt | wc -l`
+  if [ "$num" -lt $2 ]
+  then
+    fail "Expected at least $2 occurences of $1, got $num: " `cat output.log.txt`
+  fi
+}
+
+function test_execute() {
+  set_workspace_command 'repository_ctx.execute(["echo", "testing!"])'
+  build_and_process_log
+
+  ensure_contains_exactly "location: .*repos.bzl:2:3" 1
+
+  # Cached executions are not replayed
+  build_and_process_log
+  ensure_contains_exactly "location: .*repos.bzl:2:3" 0
+}
+
+# The workspace is set up so that the function is interrupted and re-executed.
+# The log should contain both instances.
 function test_reexecute() {
   create_new_workspace
   cat > BUILD <<'EOF'
@@ -108,60 +128,46 @@
 a_repo(name = "another")
 EOF
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test"
-  executes=`grep "location: .*repos.bzl:2:3" $TEST_log | wc -l`
-  if [ "$executes" -le "2" ]
-  then
-    fail "Expected at least 2 occurrences of the given command, got $executes"
-  fi
+  build_and_process_log
+
+  ensure_contains_atleast "location: .*repos.bzl:2:3" 2
 }
 
-# Sets up a workspace with the given commands inserted into the repository rule
-# that will be executed when doing bazel build //:test
-function set_workspace_command() {
-  create_new_workspace
-  cat > BUILD <<'EOF'
-genrule(
-   name="test",
-   srcs=["@repo//:t.txt"],
-   outs=["out.txt"],
-   cmd="echo Result > $(location out.txt)"
-)
-EOF
-  cat >> repos.bzl <<EOF
-def _executeMe(repository_ctx):
-  $1
-  build_contents = "package(default_visibility = ['//visibility:public'])\n\n"
-  build_contents += "exports_files([\"t.txt\"])\n"
-  repository_ctx.file("BUILD", build_contents, False)
-  repository_ctx.file("t.txt", "HELLO!\n", False)
-
-ex_repo = repository_rule(
-  implementation = _executeMe,
-  local = True,
-)
-EOF
-  cat >> WORKSPACE <<EOF
-load("//:repos.bzl", "ex_repo")
-ex_repo(name = "repo")
-EOF
-}
 
 # Ensure details of the specific functions are present
 function test_execute2() {
   set_workspace_command 'repository_ctx.execute(["echo", "test_contents"], 21, {"Arg1": "Val1"}, True)'
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> ${TEST_log} || fail "could not build //:test\n"
-  expect_log "location: .*repos.bzl:2:3"
-  expect_log "arguments: \"echo\""
-  expect_log "arguments: \"test_contents\""
-  expect_log "timeout_seconds: 21"
-  expect_log "quiet: true"
-  expect_log "key: \"Arg1\""
-  expect_log "value: \"Val1\""
-  expect_log "rule: \"//external:repo\""
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:3' 1
+  ensure_contains_exactly 'arguments: "echo"' 1
+  ensure_contains_exactly 'arguments: "test_contents"' 1
+  ensure_contains_exactly 'timeout_seconds: 21' 1
+  ensure_contains_exactly 'quiet: true' 1
+  ensure_contains_exactly 'key: "Arg1"' 1
+  ensure_contains_exactly 'value: "Val1"' 1
+  # Workspace contains 2 file commands
+  ensure_contains_atleast 'rule: "//external:repo"' 3
 }
 
+function test_execute_quiet2() {
+  set_workspace_command 'repository_ctx.execute(["echo", "test2"], 32, {"A1": "V1"}, False)'
+
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:3' 1
+  ensure_contains_exactly 'arguments: "echo"' 1
+  ensure_contains_exactly 'arguments: "test2"' 1
+  ensure_contains_exactly 'timeout_seconds: 32' 1
+  # quiet: false does not show up when printing protos
+  # since it's the default value
+  ensure_contains_exactly 'quiet: ' 0
+  ensure_contains_exactly 'key: "A1"' 1
+  ensure_contains_exactly 'value: "V1"' 1
+  # Workspace contains 2 file commands
+  ensure_contains_atleast 'rule: "//external:repo"' 3
+}
 
 function test_download() {
   # Prepare HTTP server with Python
@@ -176,13 +182,14 @@
 
   set_workspace_command "repository_ctx.download(\"http://localhost:${fileserver_port}/file.txt\", \"file.txt\", \"${file_sha256}\")"
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> ${TEST_log} && shutdown_server || fail "could not build //:test\n"
-  expect_log "location: .*repos.bzl:2:3"
-  expect_log "rule: \"//external:repo\""
-  expect_log "download_event"
-  expect_log "url: \"http://localhost:${fileserver_port}/file.txt\""
-  expect_log "output: \"file.txt\""
-  expect_log "sha256: \"${file_sha256}\""
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:3' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'download_event' 1
+  ensure_contains_exactly "url: \"http://localhost:${fileserver_port}/file.txt\"" 1
+  ensure_contains_exactly 'output: "file.txt"' 1
+  ensure_contains_exactly "sha256: \"${file_sha256}\"" 1
 }
 
 function test_download_multiple() {
@@ -197,13 +204,14 @@
 
   set_workspace_command "repository_ctx.download([\"http://localhost:${fileserver_port}/file1.txt\",\"http://localhost:${fileserver_port}/file2.txt\"], \"out_for_list.txt\")"
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log && shutdown_server || fail "could not build //:test\n"
-  expect_log "location: .*repos.bzl:2:3"
-  expect_log "rule: \"//external:repo\""
-  expect_log "download_event"
-  expect_log "url: \"http://localhost:${fileserver_port}/file1.txt\""
-  expect_log "url: \"http://localhost:${fileserver_port}/file2.txt\""
-  expect_log "output: \"out_for_list.txt\""
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:3' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'download_event' 1
+  ensure_contains_exactly "url: \"http://localhost:${fileserver_port}/file1.txt\"" 1
+  ensure_contains_exactly "url: \"http://localhost:${fileserver_port}/file2.txt\"" 1
+  ensure_contains_exactly 'output: "out_for_list.txt"' 1
 }
 
 function test_download_and_extract() {
@@ -223,74 +231,81 @@
 
   set_workspace_command "repository_ctx.download_and_extract(\"http://localhost:${fileserver_port}/download_and_extract.zip\", \"out_dir\", \"${file_sha256}\", \"zip\", \"server_dir/\")"
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> ${TEST_log} && shutdown_server || fail "could not build //:test\n"
+  build_and_process_log --exclude_rule "//external:local_config_cc"
 
-  expect_log "location: .*repos.bzl:2:3"
-  expect_log "rule: \"//external:repo\""
-  expect_log "download_and_extract_event"
-  expect_log "url: \"http://localhost:${fileserver_port}/download_and_extract.zip\""
-  expect_log "output: \"out_dir\""
-  expect_log "sha256: \"${file_sha256}\""
-  expect_log "type: \"zip\""
-  expect_log "strip_prefix: \"server_dir/\""
+  ensure_contains_exactly 'location: .*repos.bzl:2:3' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'download_and_extract_event' 1
+  ensure_contains_exactly "url: \"http://localhost:${fileserver_port}/download_and_extract.zip\"" 1
+  ensure_contains_exactly 'output: "out_dir"' 1
+  ensure_contains_exactly "sha256: \"${file_sha256}\"" 1
+  ensure_contains_exactly 'type: "zip"' 1
+  ensure_contains_exactly 'strip_prefix: "server_dir/"' 1
 }
 
 function test_file() {
   set_workspace_command 'repository_ctx.file("filefile.sh", "echo filefile", True)'
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test\n"
-  expect_log 'location: .*repos.bzl:2:3'
-  expect_log 'rule: "//external:repo"'
-  expect_log 'file_event'
-  expect_log 'path: ".*filefile.sh"'
-  expect_log 'content: "echo filefile"'
-  expect_log 'executable: true'
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:3' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+
+  # There are 3 file_event in external:repo as it is currently set up
+  ensure_contains_exactly 'file_event' 3
+  ensure_contains_exactly 'path: ".*filefile.sh"' 1
+  ensure_contains_exactly 'content: "echo filefile"' 1
+  ensure_contains_exactly 'executable: true' 1
 }
 
 function test_os() {
   set_workspace_command 'print(repository_ctx.os.name)'
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test\n"
-  expect_log 'location: .*repos.bzl:2:9'
-  expect_log 'rule: "//external:repo"'
-  expect_log 'os_event'
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:9' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'os_event' 1
 }
 
 function test_symlink() {
   set_workspace_command 'repository_ctx.file("symlink.txt", "something")
   repository_ctx.symlink("symlink.txt", "symlink_out.txt")'
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test\n"
-  expect_log 'location: .*repos.bzl:3:3'
-  expect_log 'rule: "//external:repo"'
-  expect_log 'symlink_event'
-  expect_log 'from: ".*symlink.txt"'
-  expect_log 'to: ".*symlink_out.txt"'
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:3:3' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'symlink_event' 1
+  ensure_contains_exactly 'from: ".*symlink.txt"' 1
+  ensure_contains_exactly 'to: ".*symlink_out.txt"' 1
 }
 
 function test_template() {
-  set_workspace_command 'repository_ctx.file("template_in.txt", "%{subKey}")
+  set_workspace_command 'repository_ctx.file("template_in.txt", "%{subKey}", False)
   repository_ctx.template("template_out.txt", "template_in.txt", {"subKey": "subVal"}, True)'
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test\n"
-  expect_log 'location: .*repos.bzl:3:3'
-  expect_log 'rule: "//external:repo"'
-  expect_log 'template_event'
-  expect_log 'path: ".*template_out.txt"'
-  expect_log 'template: ".*template_in.txt"'
-  expect_log 'key: "subKey"'
-  expect_log 'value: "subVal"'
-  expect_log 'executable: true'
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:3:3' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'template_event' 1
+  ensure_contains_exactly 'path: ".*template_out.txt"' 1
+  ensure_contains_exactly 'template: ".*template_in.txt"' 1
+  ensure_contains_exactly 'key: "subKey"' 1
+  ensure_contains_exactly 'value: "subVal"' 1
+  ensure_contains_exactly 'executable: true' 1
 }
 
 function test_which() {
   set_workspace_command 'print(repository_ctx.which("which_prog"))'
 
-  bazel build //:test --experimental_workspace_rules_logging=yes &> $TEST_log || fail "could not build //:test\n"
-  expect_log 'location: .*repos.bzl:2:9'
-  expect_log 'rule: "//external:repo"'
-  expect_log 'which_event'
-  expect_log 'program: "which_prog"'
+  build_and_process_log --exclude_rule "//external:local_config_cc"
+
+  ensure_contains_exactly 'location: .*repos.bzl:2:9' 1
+  ensure_contains_atleast 'rule: "//external:repo"' 1
+  ensure_contains_exactly 'which_event' 1
+  ensure_contains_exactly 'program: "which_prog"' 1
 }
 
 function tear_down() {
@@ -302,3 +317,4 @@
 }
 
 run_suite "workspaces_tests"
+
diff --git a/src/tools/workspacelog/BUILD b/src/tools/workspacelog/BUILD
new file mode 100644
index 0000000..6e741b0
--- /dev/null
+++ b/src/tools/workspacelog/BUILD
@@ -0,0 +1,15 @@
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]) + [
+        "//src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog:srcs",
+        "//src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog:srcs",
+    ],
+    visibility = ["//src:__pkg__"],
+)
+
+java_binary(
+    name = "parser",
+    main_class = "com.google.devtools.build.workspacelog.WorkspaceLogParser",
+    visibility = ["//visibility:public"],
+    runtime_deps = ["//src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog:parser"],
+)
diff --git a/src/tools/workspacelog/README.md b/src/tools/workspacelog/README.md
new file mode 100644
index 0000000..2c6b9bd
--- /dev/null
+++ b/src/tools/workspacelog/README.md
@@ -0,0 +1,38 @@
+# Workspace Log Parser
+
+This tool is used to inspect and parse the Bazel workspace logs.
+To generate the workspace log, run e.g.,:
+
+        bazel build \
+            --experimental_workspace_rules_log_file=/tmp/workspace.log :hello_world
+
+Then build the parser and run it.
+
+        bazel build src/tools/workspacelog:all
+        bazel-bin/src/tools/workspacelog/parser --log_path=/tmp/workspace.log
+
+This will simply print the log contents to stdout in text form.
+
+
+To output results to a file, use `--output_path`:
+
+        bazel-bin/src/tools/workspacelog/parser --log_path=/tmp/workspace.log \
+            --output_path=/tmp/workspace.log.txt
+
+
+To exclude all events produced by a certain rule, use `--exclude_rule`:
+
+        bazel build src/tools/workspacelog:all
+        bazel-bin/src/tools/workspacelog/parser --log_path=/tmp/workspace.log \
+            --exclude_rule "//external:local_config_cc"
+
+Note that `--exclude_rule` may be specified multiple times.
+
+        bazel build src/tools/workspacelog:all
+        bazel-bin/src/tools/workspacelog/parser --log_path=/tmp/workspace.log \
+            --exclude_rule "//external:local_config_cc" \
+            --exclude_rule "//external:dep"
+
+For example, the above will filter out any events produced by rules
+`//external:local_config_cc` or `//external:dep`
+
diff --git a/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/BUILD b/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/BUILD
new file mode 100644
index 0000000..b3df80d
--- /dev/null
+++ b/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/BUILD
@@ -0,0 +1,16 @@
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+    visibility = ["//src/tools/workspacelog:__pkg__"],
+)
+
+java_library(
+    name = "parser",
+    srcs = glob(["*.java"]),
+    visibility = ["//src/tools/workspacelog:__subpackages__"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib/bazel/debug:workspace_log_java_proto",
+        "//src/main/java/com/google/devtools/common/options",
+        "//third_party:guava",
+    ],
+)
diff --git a/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParser.java b/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParser.java
new file mode 100644
index 0000000..79f25a9
--- /dev/null
+++ b/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParser.java
@@ -0,0 +1,111 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.devtools.build.workspacelog;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.lib.bazel.debug.proto.WorkspaceLogProtos.WorkspaceEvent;
+import com.google.devtools.common.options.OptionsParser;
+import java.io.BufferedWriter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** A tool to inspect and parse the Bazel workspace rules log. */
+final class WorkspaceLogParser {
+
+  static final String DELIMITER = "\n---------------------------------------------------------\n";
+
+  @VisibleForTesting
+  static class ExcludingLogParser {
+    final InputStream in;
+    final Set<String> excludedRules;
+
+    ExcludingLogParser(InputStream in, Set<String> excludedRules) {
+      this.in = in;
+      if (excludedRules == null) {
+        this.excludedRules = Collections.emptySet();
+      } else {
+        this.excludedRules = excludedRules;
+      }
+    }
+
+    public WorkspaceEvent getNext() throws IOException {
+      WorkspaceEvent w;
+      // Find the next record whose runner matches
+      do {
+        if (in.available() <= 0) {
+          // End of file
+          return null;
+        }
+        w = WorkspaceEvent.parseDelimitedFrom(in);
+      } while (excludedRules.contains(w.getRule()));
+      return w;
+    }
+  }
+
+  public static void output(ExcludingLogParser p, OutputStream outStream) throws IOException {
+    PrintWriter out =
+        new PrintWriter(new BufferedWriter(new OutputStreamWriter(outStream, UTF_8)), true);
+    WorkspaceEvent w;
+    while ((w = p.getNext()) != null) {
+      out.println(w);
+      out.println(DELIMITER);
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    OptionsParser op = OptionsParser.newOptionsParser(WorkspaceLogParserOptions.class);
+    op.parseAndExitUponError(args);
+
+    WorkspaceLogParserOptions options = op.getOptions(WorkspaceLogParserOptions.class);
+    List<String> remainingArgs = op.getResidue();
+
+    if (!remainingArgs.isEmpty()) {
+      System.err.println("Unexpected options: " + String.join(" ", remainingArgs));
+      System.exit(1);
+    }
+
+    if (options.logPath == null || options.logPath.isEmpty()) {
+      System.err.println("--log_path needs to be specified.");
+      System.exit(1);
+    }
+
+    try (InputStream input = new FileInputStream(options.logPath)) {
+      ExcludingLogParser parser;
+      if (options.excludeRule == null) {
+        parser = new ExcludingLogParser(input, null);
+      } else {
+        parser = new ExcludingLogParser(input, new HashSet<String>(options.excludeRule));
+      }
+
+      if (options.outputPath == null) {
+        output(parser, System.out);
+      } else {
+        try (OutputStream output = new FileOutputStream(options.outputPath)) {
+          output(parser, output);
+        }
+      }
+    }
+  }
+}
diff --git a/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParserOptions.java b/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParserOptions.java
new file mode 100644
index 0000000..2598b26
--- /dev/null
+++ b/src/tools/workspacelog/src/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParserOptions.java
@@ -0,0 +1,52 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.devtools.build.workspacelog;
+
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import java.util.List;
+
+/** Options for workspace log parser. */
+public class WorkspaceLogParserOptions extends OptionsBase {
+  @Option(
+      name = "log_path",
+      defaultValue = "null",
+      category = "logging",
+      documentationCategory = OptionDocumentationCategory.LOGGING,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Location of the workspace rules log file to parse.")
+  public String logPath;
+
+  @Option(
+      name = "output_path",
+      defaultValue = "null",
+      category = "logging",
+      documentationCategory = OptionDocumentationCategory.LOGGING,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Location where to put the output. If left empty, the log will be output to stdout.")
+  public String outputPath;
+
+  @Option(
+      name = "exclude_rule",
+      defaultValue = "null",
+      category = "logging",
+      documentationCategory = OptionDocumentationCategory.LOGGING,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      allowMultiple = true,
+      help = "Rule(s) to filter out while parsing.")
+  public List<String> excludeRule;
+}
diff --git a/src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog/BUILD b/src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog/BUILD
new file mode 100644
index 0000000..759eb3e
--- /dev/null
+++ b/src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog/BUILD
@@ -0,0 +1,30 @@
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(
+        ["**"],
+        exclude = [
+            "*~",
+        ],
+    ),
+    visibility = ["//src/tools/workspacelog:__pkg__"],
+)
+
+java_test(
+    name = "WorkspaceLogParserTest",
+    size = "small",
+    srcs = ["WorkspaceLogParserTest.java"],
+    test_class = "com.google.devtools.build.workspacelog.WorkspaceLogParserTest",
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib/bazel/debug:workspace_log_java_proto",
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/tools/execlog/src/main/java/com/google/devtools/build/workspacelog:parser",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
diff --git a/src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParserTest.java b/src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParserTest.java
new file mode 100644
index 0000000..1d53fdf
--- /dev/null
+++ b/src/tools/workspacelog/test/main/java/com/google/devtools/build/workspacelog/WorkspaceLogParserTest.java
@@ -0,0 +1,125 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.devtools.build.workspacelog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.bazel.debug.proto.WorkspaceLogProtos.WorkspaceEvent;
+import com.google.devtools.build.workspacelog.WorkspaceLogParser.ExcludingLogParser;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Testing WorkspaceLogParser */
+@RunWith(JUnit4.class)
+public final class WorkspaceLogParserTest {
+
+  private InputStream toInputStream(List<WorkspaceEvent> list) throws Exception {
+    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+    for (WorkspaceEvent event : list) {
+      event.writeDelimitedTo(bos);
+    }
+
+    return new ByteArrayInputStream(bos.toByteArray());
+  }
+
+  @Test
+  public void getNextEmpty() throws Exception {
+    ExcludingLogParser p =
+        new ExcludingLogParser(toInputStream(new ArrayList<WorkspaceEvent>()), null);
+    assertThat(p.getNext()).isNull();
+  }
+
+  @Test
+  public void getNextEmptyWithExclusions() throws Exception {
+    ExcludingLogParser p =
+        new ExcludingLogParser(
+            toInputStream(new ArrayList<WorkspaceEvent>()), new HashSet<>(Arrays.asList("a", "b")));
+    assertThat(p.getNext()).isNull();
+  }
+
+  @Test
+  public void getNextSingleExcluded1() throws Exception {
+    WorkspaceEvent a = WorkspaceEvent.newBuilder().setRule("a").setLocation("SomeLocation").build();
+
+    // Excluded by first exclusion
+    ExcludingLogParser p =
+        new ExcludingLogParser(
+            toInputStream(Arrays.asList(a)), new HashSet<>(Arrays.asList("a", "b")));
+    assertThat(p.getNext()).isNull();
+  }
+
+  @Test
+  public void getNextSingleExcluded2() throws Exception {
+    WorkspaceEvent a = WorkspaceEvent.newBuilder().setRule("a").setLocation("SomeLocation").build();
+
+    // Excluded by second exclusion
+    ExcludingLogParser p =
+        new ExcludingLogParser(
+            toInputStream(Arrays.asList(a)), new HashSet<>(Arrays.asList("b", "a")));
+    assertThat(p.getNext()).isNull();
+  }
+
+  @Test
+  public void getNextSingleIncluded() throws Exception {
+    WorkspaceEvent a =
+        WorkspaceEvent.newBuilder().setRule("onOnList").setLocation("SomeLocation").build();
+
+    ExcludingLogParser p =
+        new ExcludingLogParser(
+            toInputStream(Arrays.asList(a)), new HashSet<>(Arrays.asList("b", "a")));
+    assertThat(p.getNext()).isEqualTo(a);
+    assertThat(p.getNext()).isNull();
+  }
+
+  @Test
+  public void getNextSingleLongerList1() throws Exception {
+    WorkspaceEvent a = WorkspaceEvent.newBuilder().setRule("a").setLocation("a1").build();
+    WorkspaceEvent b = WorkspaceEvent.newBuilder().setRule("b").setLocation("b1").build();
+    WorkspaceEvent c = WorkspaceEvent.newBuilder().setRule("a").setLocation("a2").build();
+    WorkspaceEvent d = WorkspaceEvent.newBuilder().setRule("b").setLocation("b2").build();
+    WorkspaceEvent e = WorkspaceEvent.newBuilder().setRule("d").build();
+
+    ExcludingLogParser p =
+        new ExcludingLogParser(
+            toInputStream(Arrays.asList(a, b, c, d, e)), new HashSet<>(Arrays.asList("b", "a")));
+    assertThat(p.getNext()).isEqualTo(e);
+    assertThat(p.getNext()).isNull();
+  }
+
+  @Test
+  public void getNextSingleLongerList2() throws Exception {
+    WorkspaceEvent a = WorkspaceEvent.newBuilder().setRule("a").setLocation("a1").build();
+    WorkspaceEvent b = WorkspaceEvent.newBuilder().setRule("b").setLocation("b1").build();
+    WorkspaceEvent c = WorkspaceEvent.newBuilder().setRule("a").setLocation("a2").build();
+    WorkspaceEvent d = WorkspaceEvent.newBuilder().setRule("b").setLocation("b2").build();
+    WorkspaceEvent e = WorkspaceEvent.newBuilder().setRule("d").build();
+
+    ExcludingLogParser p =
+        new ExcludingLogParser(
+            toInputStream(Arrays.asList(a, b, c, d, e)), new HashSet<>(Arrays.asList("d")));
+    assertThat(p.getNext()).isEqualTo(a);
+    assertThat(p.getNext()).isEqualTo(b);
+    assertThat(p.getNext()).isEqualTo(c);
+    assertThat(p.getNext()).isEqualTo(d);
+    assertThat(p.getNext()).isNull();
+  }
+}