This change adds the writing of the remote execution log to a file behind an experimental flag. It also adds a logging handler for Execute calls so that they are logged.

PiperOrigin-RevId: 190991493
diff --git a/src/main/java/com/google/devtools/build/lib/remote/BUILD b/src/main/java/com/google/devtools/build/lib/remote/BUILD
index 42bf4020..18e38ed 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/BUILD
@@ -31,6 +31,7 @@
         "//src/main/java/com/google/devtools/build/lib/exec/local:options",
         "//src/main/java/com/google/devtools/build/lib/remote/blobstore",
         "//src/main/java/com/google/devtools/build/lib/remote/blobstore/http",
+        "//src/main/java/com/google/devtools/build/lib/remote/logging",
         "//src/main/java/com/google/devtools/build/lib/remote/util",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/common/options",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
index dbb0e06..805f8f2 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.exec.ExecutorBuilder;
+import com.google.devtools.build.lib.remote.logging.LoggingInterceptor;
 import com.google.devtools.build.lib.remote.util.DigestUtil;
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.Command;
@@ -29,6 +30,7 @@
 import com.google.devtools.build.lib.runtime.ServerBuilder;
 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.build.lib.vfs.FileSystem.HashFunction;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
@@ -36,12 +38,14 @@
 import com.google.devtools.common.options.OptionsProvider;
 import com.google.devtools.remoteexecution.v1test.Digest;
 import io.grpc.Channel;
+import io.grpc.ClientInterceptors;
 import java.io.IOException;
 import java.util.logging.Logger;
 
 /** RemoteModule provides distributed cache and remote execution for Bazel. */
 public final class RemoteModule extends BlazeModule {
   private static final Logger logger = Logger.getLogger(RemoteModule.class.getName());
+  private AsynchronousFileOutputStream rpcLogFile;
 
   @VisibleForTesting
   static final class CasPathConverter implements PathConverter {
@@ -126,6 +130,10 @@
       boolean remoteOrLocalCache = SimpleBlobStoreFactory.isRemoteCacheOptions(remoteOptions);
       boolean grpcCache = GrpcRemoteCache.isRemoteCacheOptions(remoteOptions);
 
+      if (!remoteOptions.experimentalRemoteGrpcLog.isEmpty()) {
+        rpcLogFile = new AsynchronousFileOutputStream(remoteOptions.experimentalRemoteGrpcLog);
+      }
+
       RemoteRetrier retrier =
           new RemoteRetrier(
               remoteOptions, RemoteRetrier.RETRIABLE_GRPC_ERRORS, Retrier.ALLOW_ALL_CALLS);
@@ -157,9 +165,13 @@
 
       final GrpcRemoteExecutor executor;
       if (remoteOptions.remoteExecutor != null) {
+        Channel ch = GoogleAuthUtils.newChannel(remoteOptions.remoteExecutor, authAndTlsOptions);
+        if (rpcLogFile != null) {
+          ch = ClientInterceptors.intercept(ch, new LoggingInterceptor(rpcLogFile));
+        }
         executor =
             new GrpcRemoteExecutor(
-                GoogleAuthUtils.newChannel(remoteOptions.remoteExecutor, authAndTlsOptions),
+                ch,
                 GoogleAuthUtils.newCallCredentials(authAndTlsOptions),
                 remoteOptions.remoteTimeout,
                 retrier);
@@ -176,6 +188,19 @@
   }
 
   @Override
+  public void afterCommand() {
+    if (rpcLogFile != null) {
+      try {
+        rpcLogFile.close();
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      } finally {
+        rpcLogFile = null;
+      }
+    }
+  }
+
+  @Override
   public void executorInit(CommandEnvironment env, BuildRequest request, ExecutorBuilder builder) {
     if (actionContextProvider != null) {
       builder.addActionContextProvider(actionContextProvider);
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java
index e6493f3..dd4f5a0 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java
@@ -206,4 +206,20 @@
             + "writing of files, which could cause false positives."
   )
   public boolean experimentalGuardAgainstConcurrentChanges;
+
+  @Option(
+    name = "experimental_remote_grpc_log",
+    defaultValue = "",
+    category = "remote",
+    documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+    effectTags = {OptionEffectTag.UNKNOWN},
+    help =
+        "If specified, a path to a file to log gRPC call related details. This log consists "
+            + "of a sequence of serialized "
+            + "com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.LogEntry "
+            + "protobufs with each message prefixed by a varint denoting the size of the following "
+            + "serialized protobuf message, as performed by the method "
+            + "LogEntry.writeDelimitedTo(OutputStream)."
+  )
+  public String experimentalRemoteGrpcLog;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/remote/logging/BUILD b/src/main/java/com/google/devtools/build/lib/remote/logging/BUILD
index 9d8af58..cd688e1 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/logging/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/logging/BUILD
@@ -11,10 +11,12 @@
     srcs = glob(["*.java"]),
     tags = ["bazel"],
     deps = [
+        "//src/main/java/com/google/devtools/build/lib:io",
         "//src/main/java/com/google/devtools/build/lib/remote/util",
         "//src/main/protobuf:remote_execution_log_java_proto",
         "//third_party:guava",
         "//third_party/grpc:grpc-jar",
+        "@googleapis//:google_devtools_remoteexecution_v1test_remote_execution_java_grpc",
         "@googleapis//:google_devtools_remoteexecution_v1test_remote_execution_java_proto",
         "@googleapis//:google_longrunning_operations_java_proto",
         "@googleapis//:google_rpc_status_java_proto",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/logging/ExecuteHandler.java b/src/main/java/com/google/devtools/build/lib/remote/logging/ExecuteHandler.java
new file mode 100644
index 0000000..52ad59c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/remote/logging/ExecuteHandler.java
@@ -0,0 +1,45 @@
+// 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.lib.remote.logging;
+
+import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.ExecuteDetails;
+import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.RpcCallDetails;
+import com.google.devtools.remoteexecution.v1test.ExecuteRequest;
+import com.google.longrunning.Operation;
+
+/** LoggingHandler for google.devtools.remoteexecution.v1test.Execution.Execute gRPC call. */
+public class ExecuteHandler implements LoggingHandler<ExecuteRequest, Operation> {
+
+  private final ExecuteDetails.Builder builder;
+
+  public ExecuteHandler() {
+    builder = ExecuteDetails.newBuilder();
+  }
+
+  @Override
+  public void handleReq(ExecuteRequest message) {
+    builder.setRequest(message);
+  }
+
+  @Override
+  public void handleResp(Operation message) {
+    builder.setResponse(message);
+  }
+
+  @Override
+  public RpcCallDetails getDetails() {
+    return RpcCallDetails.newBuilder().setExecute(builder).build();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingHandler.java b/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingHandler.java
index fb23d6c..d9653a8 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingHandler.java
@@ -14,11 +14,10 @@
 
 package com.google.devtools.build.lib.remote.logging;
 
-import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.LogEntry;
+import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.RpcCallDetails;
 
 /**
- * An interface for building {@link LogEntry}s specialized for a specific gRPC call with specific
- * request and response types.
+ * An interface for building {@link RpcCallDetails}s specialized for a specific gRPC call.
  *
  * @param <ReqT> request type of the gRPC call
  * @param <RespT> response type of the gRPC call
@@ -39,6 +38,8 @@
    */
   void handleResp(RespT message);
 
-  /** Returns a {@link LogEntry} based on the requests and responses handled by this handler * */
-  LogEntry getEntry();
+  /**
+   * Returns a {@link RpcCallDetails} based on the requests and responses handled by this handler.
+   */
+  RpcCallDetails getDetails();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptor.java b/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptor.java
index 37629d7..b46d4e0 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptor.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptor.java
@@ -16,6 +16,8 @@
 
 import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.LogEntry;
 import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
+import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
+import com.google.devtools.remoteexecution.v1test.ExecutionGrpc;
 import com.google.devtools.remoteexecution.v1test.RequestMetadata;
 import io.grpc.CallOptions;
 import io.grpc.Channel;
@@ -30,6 +32,12 @@
 
 /** Client interceptor for logging details of certain gRPC calls. */
 public class LoggingInterceptor implements ClientInterceptor {
+  AsynchronousFileOutputStream rpcLogFile;
+
+  /** Constructs a LoggingInterceptor which logs RPC calls to the given file. */
+  public LoggingInterceptor(AsynchronousFileOutputStream rpcLogFile) {
+    this.rpcLogFile = rpcLogFile;
+  }
 
   /**
    * Returns a {@link LoggingHandler} to handle logging details for the specified method. If there
@@ -37,9 +45,12 @@
    *
    * @param method Method to return handler for.
    */
-  protected <ReqT, RespT> @Nullable LoggingHandler<ReqT, RespT> selectHandler(
+  @SuppressWarnings("rawtypes")
+  protected <ReqT, RespT> @Nullable LoggingHandler selectHandler(
       MethodDescriptor<ReqT, RespT> method) {
-    // TODO(cdlee): add handlers for methods
+    if (method == ExecutionGrpc.getExecuteMethod()) {
+      return new ExecuteHandler();
+    }
     return null;
   }
 
@@ -56,9 +67,10 @@
   }
 
   /**
-   * Wraps client call to log call details by building a {@link LogEntry} and writing it to a log.
+   * Wraps client call to log call details by building a {@link LogEntry} and writing it to the RPC
+   * log file.
    */
-  private static class LoggingForwardingCall<ReqT, RespT>
+  private class LoggingForwardingCall<ReqT, RespT>
       extends ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT> {
     private final LoggingHandler<ReqT, RespT> handler;
     private final LogEntry.Builder entryBuilder;
@@ -90,8 +102,8 @@
             @Override
             public void onClose(Status status, Metadata trailers) {
               entryBuilder.setStatus(makeStatusProto(status));
-              // TODO(cdlee): Actually store this and log the entry.
-              entryBuilder.mergeFrom(handler.getEntry()).build();
+              entryBuilder.setDetails(handler.getDetails());
+              rpcLogFile.write(entryBuilder.build());
               super.onClose(status, trailers);
             }
           },
diff --git a/src/main/protobuf/BUILD b/src/main/protobuf/BUILD
index 4154a17..3ed9d1f 100644
--- a/src/main/protobuf/BUILD
+++ b/src/main/protobuf/BUILD
@@ -133,6 +133,7 @@
     srcs = ["remote_execution_log.proto"],
     deps = [
         "@googleapis//:google_devtools_remoteexecution_v1test_remote_execution_proto",
+        "@googleapis//:google_longrunning_operations_proto",
         "@googleapis//:google_rpc_status_proto",
     ],
 )
diff --git a/src/main/protobuf/remote_execution_log.proto b/src/main/protobuf/remote_execution_log.proto
index c685bd3..81c8de9 100644
--- a/src/main/protobuf/remote_execution_log.proto
+++ b/src/main/protobuf/remote_execution_log.proto
@@ -17,6 +17,7 @@
 package remote_logging;
 
 import "google/devtools/remoteexecution/v1test/remote_execution.proto";
+import "google/longrunning/operations.proto";
 import "google/rpc/status.proto";
 
 option java_package = "com.google.devtools.build.lib.remote.logging";
@@ -33,4 +34,24 @@
   // io.grpc.MethodDescriptor.getFullMethodName() (i.e. in format
   // $FULL_SERVICE_NAME/$METHOD_NAME).
   string method_name = 3;
+
+  // Method specific details for this call.
+  RpcCallDetails details = 4;
+}
+
+// Details for a call to
+// google.devtools.remoteexecution.v1test.Execution.Execute.
+message ExecuteDetails {
+  // The google.devtools.remoteexecution.v1test.ExecuteRequest sent by the
+  // call.
+  google.devtools.remoteexecution.v1test.ExecuteRequest request = 1;
+  // The google.longrunning.Operation received by the Execute call.
+  google.longrunning.Operation response = 2;
+}
+
+// Contains details for specific types of calls.
+message RpcCallDetails {
+  oneof details {
+    ExecuteDetails execute = 1;
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptorTest.java b/src/test/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptorTest.java
index b9c3d54..66221c7 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/logging/LoggingInterceptorTest.java
@@ -30,7 +30,16 @@
 import com.google.bytestream.ByteStreamProto.ReadResponse;
 import com.google.bytestream.ByteStreamProto.WriteRequest;
 import com.google.bytestream.ByteStreamProto.WriteResponse;
+import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.ExecuteDetails;
 import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.LogEntry;
+import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.RpcCallDetails;
+import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
+import com.google.devtools.remoteexecution.v1test.Action;
+import com.google.devtools.remoteexecution.v1test.ExecuteRequest;
+import com.google.devtools.remoteexecution.v1test.ExecutionGrpc;
+import com.google.devtools.remoteexecution.v1test.ExecutionGrpc.ExecutionBlockingStub;
+import com.google.devtools.remoteexecution.v1test.ExecutionGrpc.ExecutionImplBase;
+import com.google.longrunning.Operation;
 import com.google.protobuf.ByteString;
 import io.grpc.Channel;
 import io.grpc.ClientInterceptors;
@@ -59,8 +68,9 @@
 
   // This returns a logging interceptor where all calls are handled by the given handler.
   @SuppressWarnings({"rawtypes", "unchecked"})
-  private LoggingInterceptor getInterceptorWithAlwaysThisHandler(LoggingHandler handler) {
-    return new LoggingInterceptor() {
+  private LoggingInterceptor getInterceptorWithAlwaysThisHandler(
+      LoggingHandler handler, AsynchronousFileOutputStream outputFile) {
+    return new LoggingInterceptor(outputFile) {
       @Override
       public <ReqT, RespT> LoggingHandler<ReqT, RespT> selectHandler(
           MethodDescriptor<ReqT, RespT> method) {
@@ -103,18 +113,28 @@
 
     @SuppressWarnings("unchecked")
     LoggingHandler<ReadRequest, ReadResponse> handler = Mockito.mock(LoggingHandler.class);
-    Mockito.when(handler.getEntry()).thenReturn(LogEntry.getDefaultInstance());
+    RpcCallDetails details = RpcCallDetails.getDefaultInstance();
+    Mockito.when(handler.getDetails()).thenReturn(details);
+    AsynchronousFileOutputStream output = Mockito.mock(AsynchronousFileOutputStream.class);
 
-    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler);
+    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler, output);
     Channel channel =
         ClientInterceptors.intercept(
             InProcessChannelBuilder.forName(fakeServerName).directExecutor().build(), interceptor);
     ByteStreamBlockingStub stub = ByteStreamGrpc.newBlockingStub(channel);
 
+    LogEntry expectedEntry =
+        LogEntry.newBuilder()
+            .setMethodName(ByteStreamGrpc.getReadMethod().getFullMethodName())
+            .setDetails(details)
+            .setStatus(com.google.rpc.Status.getDefaultInstance())
+            .build();
+
     stub.read(request).next();
     verify(handler).handleReq(request);
     verify(handler).handleResp(response);
-    verify(handler).getEntry();
+    verify(handler).getDetails();
+    verify(output).write(expectedEntry);
   }
 
   @Test
@@ -136,9 +156,11 @@
 
     @SuppressWarnings("unchecked")
     LoggingHandler<ReadRequest, ReadResponse> handler = Mockito.mock(LoggingHandler.class);
-    Mockito.when(handler.getEntry()).thenReturn(LogEntry.getDefaultInstance());
+    RpcCallDetails details = RpcCallDetails.getDefaultInstance();
+    Mockito.when(handler.getDetails()).thenReturn(details);
+    AsynchronousFileOutputStream output = Mockito.mock(AsynchronousFileOutputStream.class);
 
-    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler);
+    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler, output);
     Channel channel =
         ClientInterceptors.intercept(
             InProcessChannelBuilder.forName(fakeServerName).directExecutor().build(), interceptor);
@@ -149,11 +171,19 @@
 
     ArgumentCaptor<ReadResponse> resultCaptor = ArgumentCaptor.forClass(ReadResponse.class);
 
+    LogEntry expectedEntry =
+        LogEntry.newBuilder()
+            .setMethodName(ByteStreamGrpc.getReadMethod().getFullMethodName())
+            .setDetails(details)
+            .setStatus(com.google.rpc.Status.getDefaultInstance())
+            .build();
+
     verify(handler).handleReq(request);
     verify(handler, times(2)).handleResp(resultCaptor.capture());
     assertThat(resultCaptor.getAllValues().get(0)).isEqualTo(response1);
     assertThat(resultCaptor.getAllValues().get(1)).isEqualTo(response2);
-    verify(handler).getEntry();
+    verify(handler).getDetails();
+    verify(output).write(expectedEntry);
   }
 
   @Test
@@ -191,9 +221,11 @@
 
     @SuppressWarnings("unchecked")
     LoggingHandler<WriteRequest, WriteResponse> handler = Mockito.mock(LoggingHandler.class);
-    Mockito.when(handler.getEntry()).thenReturn(LogEntry.getDefaultInstance());
+    RpcCallDetails details = RpcCallDetails.getDefaultInstance();
+    Mockito.when(handler.getDetails()).thenReturn(details);
+    AsynchronousFileOutputStream output = Mockito.mock(AsynchronousFileOutputStream.class);
 
-    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler);
+    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler, output);
     Channel channel =
         ClientInterceptors.intercept(
             InProcessChannelBuilder.forName(fakeServerName).directExecutor().build(), interceptor);
@@ -209,11 +241,19 @@
 
     ArgumentCaptor<WriteRequest> resultCaptor = ArgumentCaptor.forClass(WriteRequest.class);
 
+    LogEntry expectedEntry =
+        LogEntry.newBuilder()
+            .setMethodName(ByteStreamGrpc.getWriteMethod().getFullMethodName())
+            .setDetails(details)
+            .setStatus(com.google.rpc.Status.getDefaultInstance())
+            .build();
+
     verify(handler, times(2)).handleReq(resultCaptor.capture());
     assertThat(resultCaptor.getAllValues().get(0)).isEqualTo(request1);
     assertThat(resultCaptor.getAllValues().get(1)).isEqualTo(request2);
     verify(handler).handleResp(response);
-    verify(handler).getEntry();
+    verify(handler).getDetails();
+    verify(output).write(expectedEntry);
   }
 
   @Test
@@ -231,9 +271,11 @@
 
     @SuppressWarnings("unchecked")
     LoggingHandler<ReadRequest, ReadResponse> handler = Mockito.mock(LoggingHandler.class);
-    Mockito.when(handler.getEntry()).thenReturn(LogEntry.getDefaultInstance());
+    RpcCallDetails details = RpcCallDetails.getDefaultInstance();
+    Mockito.when(handler.getDetails()).thenReturn(details);
+    AsynchronousFileOutputStream output = Mockito.mock(AsynchronousFileOutputStream.class);
 
-    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler);
+    LoggingInterceptor interceptor = getInterceptorWithAlwaysThisHandler(handler, output);
     Channel channel =
         ClientInterceptors.intercept(
             InProcessChannelBuilder.forName(fakeServerName).directExecutor().build(), interceptor);
@@ -241,8 +283,57 @@
 
     assertThrows(StatusRuntimeException.class, () -> stub.read(request).next());
 
+    LogEntry expectedEntry =
+        LogEntry.newBuilder()
+            .setMethodName(ByteStreamGrpc.getReadMethod().getFullMethodName())
+            .setDetails(details)
+            .setStatus(
+                com.google.rpc.Status.newBuilder()
+                    .setCode(error.getCode().value())
+                    .setMessage("not found"))
+            .build();
+
     verify(handler).handleReq(request);
     verify(handler, never()).handleResp(any());
-    verify(handler).getEntry();
+    verify(handler).getDetails();
+    verify(output).write(expectedEntry);
+  }
+
+  @Test
+  public void testExecuteCallOk() {
+    ExecuteRequest request =
+        ExecuteRequest.newBuilder()
+            .setInstanceName("test-instance")
+            .setAction(Action.newBuilder().addOutputFiles("somefile"))
+            .build();
+    Operation response = Operation.newBuilder().setName("test-operation").build();
+    serviceRegistry.addService(
+        new ExecutionImplBase() {
+          @Override
+          public void execute(ExecuteRequest request, StreamObserver<Operation> responseObserver) {
+            responseObserver.onNext(response);
+            responseObserver.onCompleted();
+          }
+        });
+
+    @SuppressWarnings("unchecked")
+    AsynchronousFileOutputStream output = Mockito.mock(AsynchronousFileOutputStream.class);
+    LoggingInterceptor interceptor = new LoggingInterceptor(output);
+    Channel channel =
+        ClientInterceptors.intercept(
+            InProcessChannelBuilder.forName(fakeServerName).directExecutor().build(), interceptor);
+    ExecutionBlockingStub stub = ExecutionGrpc.newBlockingStub(channel);
+
+    stub.execute(request);
+    LogEntry expectedEntry =
+        LogEntry.newBuilder()
+            .setMethodName(ExecutionGrpc.getExecuteMethod().getFullMethodName())
+            .setDetails(
+                RpcCallDetails.newBuilder()
+                    .setExecute(
+                        ExecuteDetails.newBuilder().setRequest(request).setResponse(response)))
+            .setStatus(com.google.rpc.Status.getDefaultInstance())
+            .build();
+    verify(output).write(expectedEntry);
   }
 }