Skip to content

Commit fe4cf5d

Browse files
committed
Add support for capturing server logs
REv2's ExecuteResponse provides a field named server_logs. This is a place where the execution environment can attach things like log files that are generated by the remote execution environment, so that they can be passed back to the client. I think that in addition to log files, it's a fairly useful place for storing core dumps of actions that crashed. This change extends bb_worker to give every action its own server_logs directory. We pass the path of this directory on to the runner. bb_runner doesn't do anything with it yet, but if you end up writing your own runner, this is where it can dump its own logs if the needs arises. With regards to writing core dumps into this directory, this is where it gets a bit tricky. On Linux, you'd ideally want to set /proc/sys/kernel/core_pattern to something like this: /worker/build/???/server_logs/coredump.timestamp=%t.pid=%p.executable=%e But what should you fill in for the question marks? That part happens to be dynamic. If you're making use of bb_runner's symlink_temporary_directories option to symlink /tmp to the per-action temporary directory, you're in luck. You can just use a pattern like this: /tmp/../server_logs/coredump.timestamp=%t.pid=%p.executable=%e This is guaranteed to work, because the server_logs directory always lives right next to the per-action temporary directory. Unfortunately, when using FUSE you're still not there. It turns out that the kernel isn't willing to write core dumps to file systems that don't track accurate file ownership and permissions. Fortunately, you can proc_pattern's pipe feature to work around that: |/usr/bin/dd conv=excl of=/tmp/../server_logs/coredump.timestamp=%t.pid=%p.executable=%e You do need to keep in mind such a helper process is always run in the root namespace, and also runs as root. So instead of doing that, I'd recommend using a command line the one below: |/usr/bin/nsenter -t %P -S %u -G %g -m /usr/bin/dd conv=excl of=/tmp/../server_logs/coredump.timestamp=%t.pid=%p.executable=%e That way it runs dd inside the container of the crashing process, and also runs as the same (non-root) user.
1 parent 4b3a11b commit fe4cf5d

File tree

5 files changed

+150
-38
lines changed

5 files changed

+150
-38
lines changed

pkg/builder/build_executor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func NewDefaultExecuteResponse(request *remoteworker.DesiredState_Executing) *re
2323
AuxiliaryMetadata: append([]*anypb.Any(nil), request.AuxiliaryMetadata...),
2424
},
2525
},
26+
ServerLogs: map[string]*remoteexecution.LogFile{},
2627
}
2728
}
2829

pkg/builder/local_build_executor.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ import (
2727

2828
// Filenames of objects to be created inside the build directory.
2929
var (
30-
stdoutComponent = path.MustNewComponent("stdout")
31-
stderrComponent = path.MustNewComponent("stderr")
32-
deviceDirectoryComponent = path.MustNewComponent("dev")
33-
inputRootDirectoryComponent = path.MustNewComponent("root")
34-
temporaryDirectoryComponent = path.MustNewComponent("tmp")
35-
checkReadinessComponent = path.MustNewComponent("check_readiness")
30+
stdoutComponent = path.MustNewComponent("stdout")
31+
stderrComponent = path.MustNewComponent("stderr")
32+
deviceDirectoryComponent = path.MustNewComponent("dev")
33+
inputRootDirectoryComponent = path.MustNewComponent("root")
34+
serverLogsDirectoryComponent = path.MustNewComponent("server_logs")
35+
temporaryDirectoryComponent = path.MustNewComponent("tmp")
36+
checkReadinessComponent = path.MustNewComponent("check_readiness")
3637
)
3738

3839
// capturingErrorLogger is an error logger that stores up to a single
@@ -243,6 +244,13 @@ func (be *localBuildExecutor) Execute(ctx context.Context, filePool re_filesyste
243244
return response
244245
}
245246

247+
if err := buildDirectory.Mkdir(serverLogsDirectoryComponent, 0o777); err != nil {
248+
attachErrorToExecuteResponse(
249+
response,
250+
util.StatusWrap(err, "Failed to create server logs directory inside build directory"))
251+
return response
252+
}
253+
246254
executionStateUpdates <- &remoteworker.CurrentState_Executing{
247255
ActionDigest: request.ActionDigest,
248256
ExecutionState: &remoteworker.CurrentState_Executing_Running{
@@ -268,6 +276,7 @@ func (be *localBuildExecutor) Execute(ctx context.Context, filePool re_filesyste
268276
StderrPath: buildDirectoryPath.Append(stderrComponent).String(),
269277
InputRootDirectory: buildDirectoryPath.Append(inputRootDirectoryComponent).String(),
270278
TemporaryDirectory: buildDirectoryPath.Append(temporaryDirectoryComponent).String(),
279+
ServerLogsDirectory: buildDirectoryPath.Append(serverLogsDirectoryComponent).String(),
271280
})
272281
cancelTimeout()
273282
<-ctxWithTimeout.Done()
@@ -317,5 +326,52 @@ func (be *localBuildExecutor) Execute(ctx context.Context, filePool re_filesyste
317326
attachErrorToExecuteResponse(response, err)
318327
}
319328

329+
// Recursively traverse the server logs directory and attach any
330+
// file stored within to the ExecuteResponse.
331+
serverLogsDirectoryUploader := serverLogsDirectoryUploader{
332+
context: ctx,
333+
executeResponse: response,
334+
digestFunction: digestFunction,
335+
}
336+
serverLogsDirectoryUploader.uploadDirectory(buildDirectory, serverLogsDirectoryComponent, nil)
337+
320338
return response
321339
}
340+
341+
type serverLogsDirectoryUploader struct {
342+
context context.Context
343+
executeResponse *remoteexecution.ExecuteResponse
344+
digestFunction digest.Function
345+
}
346+
347+
func (u *serverLogsDirectoryUploader) uploadDirectory(parentDirectory UploadableDirectory, dName path.Component, dPath *path.Trace) {
348+
d, err := parentDirectory.EnterUploadableDirectory(dName)
349+
if err != nil {
350+
attachErrorToExecuteResponse(u.executeResponse, util.StatusWrapf(err, "Failed to enter server logs directory %#v", dPath.String()))
351+
return
352+
}
353+
defer d.Close()
354+
355+
files, err := d.ReadDir()
356+
if err != nil {
357+
attachErrorToExecuteResponse(u.executeResponse, util.StatusWrapf(err, "Failed to read server logs directory %#v", dPath.String()))
358+
return
359+
}
360+
361+
for _, file := range files {
362+
childName := file.Name()
363+
childPath := dPath.Append(childName)
364+
switch fileType := file.Type(); fileType {
365+
case filesystem.FileTypeRegularFile:
366+
if childDigest, err := d.UploadFile(u.context, childName, u.digestFunction); err == nil {
367+
u.executeResponse.ServerLogs[childPath.String()] = &remoteexecution.LogFile{
368+
Digest: childDigest.GetProto(),
369+
}
370+
} else {
371+
attachErrorToExecuteResponse(u.executeResponse, util.StatusWrapf(err, "Failed to store server log %#v", childPath.String()))
372+
}
373+
case filesystem.FileTypeDirectory:
374+
u.uploadDirectory(d, childName, childPath)
375+
}
376+
}
377+
}

pkg/builder/local_build_executor_test.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ func TestLocalBuildExecutorOutputSymlinkReadingFailure(t *testing.T) {
374374
monitor,
375375
).Return(nil)
376376
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("tmp"), os.FileMode(0o777))
377+
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("server_logs"), os.FileMode(0o777))
377378
runner := mock.NewMockRunnerClient(ctrl)
378379
runner.EXPECT().Run(gomock.Any(), &runner_pb.RunRequest{
379380
Arguments: []string{"touch", "foo"},
@@ -383,6 +384,7 @@ func TestLocalBuildExecutorOutputSymlinkReadingFailure(t *testing.T) {
383384
StderrPath: "stderr",
384385
InputRootDirectory: "root",
385386
TemporaryDirectory: "tmp",
387+
ServerLogsDirectory: "server_logs",
386388
}).Return(&runner_pb.RunResponse{
387389
ExitCode: 0,
388390
}, nil)
@@ -395,6 +397,10 @@ func TestLocalBuildExecutorOutputSymlinkReadingFailure(t *testing.T) {
395397
fooDirectory.EXPECT().Readlink(path.MustNewComponent("bar")).Return("", status.Error(codes.Internal, "Cosmic rays caused interference"))
396398
fooDirectory.EXPECT().Close()
397399
inputRootDirectory.EXPECT().Close()
400+
serverLogsDirectory := mock.NewMockUploadableDirectory(ctrl)
401+
buildDirectory.EXPECT().EnterUploadableDirectory(path.MustNewComponent("server_logs")).Return(serverLogsDirectory, nil)
402+
serverLogsDirectory.EXPECT().ReadDir()
403+
serverLogsDirectory.EXPECT().Close()
398404
buildDirectory.EXPECT().Close()
399405
clock := mock.NewMockClock(ctrl)
400406
clock.EXPECT().NewContextWithTimeout(gomock.Any(), time.Hour).DoAndReturn(func(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
@@ -573,6 +579,7 @@ func TestLocalBuildExecutorSuccess(t *testing.T) {
573579
filesystem.NewDeviceNumberFromMajorMinor(1, 3))
574580
inputRootDevDirectory.EXPECT().Close()
575581
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("tmp"), os.FileMode(0o777))
582+
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("server_logs"), os.FileMode(0o777))
576583
resourceUsage, err := anypb.New(&emptypb.Empty{})
577584
require.NoError(t, err)
578585
runner := mock.NewMockRunnerClient(ctrl)
@@ -593,16 +600,21 @@ func TestLocalBuildExecutorSuccess(t *testing.T) {
593600
"PWD": "/proc/self/cwd",
594601
"TEST_VAR": "123",
595602
},
596-
WorkingDirectory: "",
597-
StdoutPath: "0000000000000000/stdout",
598-
StderrPath: "0000000000000000/stderr",
599-
InputRootDirectory: "0000000000000000/root",
600-
TemporaryDirectory: "0000000000000000/tmp",
603+
WorkingDirectory: "",
604+
StdoutPath: "0000000000000000/stdout",
605+
StderrPath: "0000000000000000/stderr",
606+
InputRootDirectory: "0000000000000000/root",
607+
TemporaryDirectory: "0000000000000000/tmp",
608+
ServerLogsDirectory: "0000000000000000/server_logs",
601609
}).Return(&runner_pb.RunResponse{
602610
ExitCode: 0,
603611
ResourceUsage: []*anypb.Any{resourceUsage},
604612
}, nil)
605613
inputRootDirectory.EXPECT().Close()
614+
serverLogsDirectory := mock.NewMockUploadableDirectory(ctrl)
615+
buildDirectory.EXPECT().EnterUploadableDirectory(path.MustNewComponent("server_logs")).Return(serverLogsDirectory, nil)
616+
serverLogsDirectory.EXPECT().ReadDir()
617+
serverLogsDirectory.EXPECT().Close()
606618
buildDirectory.EXPECT().Close()
607619
clock := mock.NewMockClock(ctrl)
608620
clock.EXPECT().NewContextWithTimeout(gomock.Any(), time.Hour).DoAndReturn(func(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
@@ -764,6 +776,7 @@ func TestLocalBuildExecutorInputRootIOFailureDuringExecution(t *testing.T) {
764776
return nil
765777
})
766778
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("tmp"), os.FileMode(0o777))
779+
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("server_logs"), os.FileMode(0o777))
767780

768781
// Let an I/O error in the input root trigger during the build.
769782
// The build should be canceled immediately. The error should be
@@ -777,12 +790,17 @@ func TestLocalBuildExecutorInputRootIOFailureDuringExecution(t *testing.T) {
777790
StderrPath: "stderr",
778791
InputRootDirectory: "root",
779792
TemporaryDirectory: "tmp",
793+
ServerLogsDirectory: "server_logs",
780794
}).DoAndReturn(func(ctx context.Context, request *runner_pb.RunRequest, opts ...grpc.CallOption) (*runner_pb.RunResponse, error) {
781795
errorLogger.Log(status.Error(codes.FailedPrecondition, "Blob not found"))
782796
<-ctx.Done()
783797
return nil, util.StatusFromContext(ctx)
784798
})
785799
inputRootDirectory.EXPECT().Close()
800+
serverLogsDirectory := mock.NewMockUploadableDirectory(ctrl)
801+
buildDirectory.EXPECT().EnterUploadableDirectory(path.MustNewComponent("server_logs")).Return(serverLogsDirectory, nil)
802+
serverLogsDirectory.EXPECT().ReadDir()
803+
serverLogsDirectory.EXPECT().Close()
786804
buildDirectory.EXPECT().Close()
787805
clock := mock.NewMockClock(ctrl)
788806
clock.EXPECT().NewContextWithTimeout(gomock.Any(), 15*time.Minute).DoAndReturn(func(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
@@ -871,6 +889,7 @@ func TestLocalBuildExecutorTimeoutDuringExecution(t *testing.T) {
871889
monitor,
872890
).Return(nil)
873891
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("tmp"), os.FileMode(0o777))
892+
buildDirectory.EXPECT().Mkdir(path.MustNewComponent("server_logs"), os.FileMode(0o777))
874893

875894
// Simulate a timeout by running the command with a timeout of
876895
// zero seconds. This should cause an immediate build failure.
@@ -883,11 +902,24 @@ func TestLocalBuildExecutorTimeoutDuringExecution(t *testing.T) {
883902
StderrPath: "stderr",
884903
InputRootDirectory: "root",
885904
TemporaryDirectory: "tmp",
905+
ServerLogsDirectory: "server_logs",
886906
}).DoAndReturn(func(ctx context.Context, request *runner_pb.RunRequest, opts ...grpc.CallOption) (*runner_pb.RunResponse, error) {
887907
<-ctx.Done()
888908
return nil, util.StatusFromContext(ctx)
889909
})
890910
inputRootDirectory.EXPECT().Close()
911+
912+
// Let the server logs directory contain a log file. It should
913+
// get attached to the ExecuteResponse.
914+
serverLogsDirectory := mock.NewMockUploadableDirectory(ctrl)
915+
buildDirectory.EXPECT().EnterUploadableDirectory(path.MustNewComponent("server_logs")).Return(serverLogsDirectory, nil)
916+
serverLogsDirectory.EXPECT().ReadDir().Return([]filesystem.FileInfo{
917+
filesystem.NewFileInfo(path.MustNewComponent("kernel_log"), filesystem.FileTypeRegularFile, false),
918+
}, nil)
919+
serverLogsDirectory.EXPECT().UploadFile(ctx, path.MustNewComponent("kernel_log"), gomock.Any()).Return(
920+
digest.MustNewDigest("ubuntu1804", remoteexecution.DigestFunction_SHA256, "53855840865bc43fa60c2e25383165017cfc3c2243541f8e6c648f5fbd374eb5", 1200),
921+
nil)
922+
serverLogsDirectory.EXPECT().Close()
891923
buildDirectory.EXPECT().Close()
892924
clock := mock.NewMockClock(ctrl)
893925
clock.EXPECT().NewContextWithTimeout(gomock.Any(), time.Hour).DoAndReturn(func(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
@@ -931,6 +963,14 @@ func TestLocalBuildExecutorTimeoutDuringExecution(t *testing.T) {
931963
},
932964
ExecutionMetadata: &remoteexecution.ExecutedActionMetadata{},
933965
},
966+
ServerLogs: map[string]*remoteexecution.LogFile{
967+
"kernel_log": {
968+
Digest: &remoteexecution.Digest{
969+
Hash: "53855840865bc43fa60c2e25383165017cfc3c2243541f8e6c648f5fbd374eb5",
970+
SizeBytes: 1200,
971+
},
972+
},
973+
},
934974
Status: status.New(codes.DeadlineExceeded, "Failed to run command: context deadline exceeded").Proto(),
935975
}, executeResponse)
936976
}

pkg/proto/runner/runner.pb.go

Lines changed: 38 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/proto/runner/runner.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ message RunRequest {
5454
// Path of a scratch space directory that may be used by the build
5555
// action, relative to the build directory.
5656
string temporary_directory = 7;
57+
58+
// Path where files may be stored that are attached to the REv2
59+
// ExecuteResponse in the form of server logs.
60+
string server_logs_directory = 8;
5761
}
5862

5963
message RunResponse {

0 commit comments

Comments
 (0)