Skip to content

Commit c567876

Browse files
committed
Refactor BufferedLogger flushing and container command execution
- Add Flush(ctx, err) to BufferedLogger to automatically flush at ERROR on failure or DEBUG on success. - Add executeWithLogger and executeWithOutput helpers to eliminate repetitive flush boilerplate. - Unify runtime dependency log outputs into single log entries. - Add unit tests and docstrings for BufferedLogger.
1 parent f87beee commit c567876

4 files changed

Lines changed: 115 additions & 39 deletions

File tree

sdks/go/container/tools/buffered_logging.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ func NewBufferedLoggerWithFlushInterval(ctx context.Context, logger *Logger, int
5252
return &BufferedLogger{logger: logger, lastFlush: time.Now(), flushInterval: interval, periodicFlushContext: ctx, now: time.Now}
5353
}
5454

55-
// Write implements the io.Writer interface, converting input to a string
56-
// and storing it in the BufferedLogger's buffer. If a logger is not provided,
57-
// the output is sent directly to os.Stderr.
55+
// Write implements the io.Writer interface. It buffers byte streams line-by-line
56+
// into memory and flushes periodically or upon calling Flush(), FlushAtError(), or
57+
// FlushAtDebug(). It is used primarily to redirect stdout/stderr of subprocesses or
58+
// standard Go log output. If a logger is not provided, the output is sent directly to os.Stderr.
5859
func (b *BufferedLogger) Write(p []byte) (int, error) {
5960
if b.logger == nil {
6061
return os.Stderr.Write(p)
@@ -86,6 +87,18 @@ func (b *BufferedLogger) Write(p []byte) (int, error) {
8687
return n, err
8788
}
8889

90+
// Flush flushes the contents of the buffer to the logging service.
91+
// If err is non-nil, it flushes at Error severity; otherwise it flushes at Debug severity.
92+
// It returns the provided error.
93+
func (b *BufferedLogger) Flush(ctx context.Context, err error) error {
94+
if err != nil {
95+
b.FlushAtError(ctx)
96+
} else {
97+
b.FlushAtDebug(ctx)
98+
}
99+
return err
100+
}
101+
89102
// FlushAtError flushes the contents of the buffer to the logging
90103
// service at Error.
91104
func (b *BufferedLogger) FlushAtError(ctx context.Context) {
@@ -120,8 +133,9 @@ func (b *BufferedLogger) FlushAtDebug(ctx context.Context) {
120133
b.lastFlush = time.Now()
121134
}
122135

123-
// Prints directly to the logging service. If the logger is nil, prints directly to the
124-
// console. Used for the container pre-build workflow.
136+
// Printf directly writes formatted messages to the underlying logger/service,
137+
// bypassing line buffering. If the logger is nil, it prints directly to the
138+
// console. Used for direct informational logs and the container pre-build workflow.
125139
func (b *BufferedLogger) Printf(ctx context.Context, format string, args ...any) {
126140
if b.logger == nil {
127141
log.Printf(format, args...)

sdks/go/container/tools/buffered_logging_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package tools
1717

1818
import (
1919
"context"
20+
"errors"
2021
"testing"
2122
"time"
2223

@@ -34,6 +35,24 @@ func getAllLogEntries(catcher *logCatcher) []*fnpb.LogEntry {
3435
func TestBufferedLogger(t *testing.T) {
3536
ctx := context.Background()
3637

38+
t.Run("printf", func(t *testing.T) {
39+
catcher := &logCatcher{}
40+
l := &Logger{client: catcher}
41+
bl := NewBufferedLogger(l)
42+
43+
bl.Printf(ctx, "test message")
44+
45+
received := catcher.msgs[0].GetLogEntries()[0]
46+
47+
if got, want := received.Message, "test message"; got != want {
48+
t.Errorf("got message %q, want %q", got, want)
49+
}
50+
51+
if got, want := received.Severity, fnpb.LogEntry_Severity_DEBUG; got != want {
52+
t.Errorf("got severity %v, want %v", got, want)
53+
}
54+
})
55+
3756
t.Run("write", func(t *testing.T) {
3857
catcher := &logCatcher{}
3958
l := &Logger{client: catcher}
@@ -186,6 +205,55 @@ func TestBufferedLogger(t *testing.T) {
186205
}
187206
})
188207

208+
t.Run("flush with nil error", func(t *testing.T) {
209+
catcher := &logCatcher{}
210+
l := &Logger{client: catcher}
211+
bl := NewBufferedLogger(l)
212+
213+
message := []byte("success message\n")
214+
_, err := bl.Write(message)
215+
if err != nil {
216+
t.Fatalf("unexpected write error: %v", err)
217+
}
218+
219+
if gotErr := bl.Flush(ctx, nil); gotErr != nil {
220+
t.Errorf("Flush(ctx, nil) returned error %v, want nil", gotErr)
221+
}
222+
223+
received := catcher.msgs[0].GetLogEntries()[0]
224+
if got, want := received.Message, "success message"; got != want {
225+
t.Errorf("got message %q, want %q", got, want)
226+
}
227+
if got, want := received.Severity, fnpb.LogEntry_Severity_DEBUG; got != want {
228+
t.Errorf("got severity %v, want %v", got, want)
229+
}
230+
})
231+
232+
t.Run("flush with non-nil error", func(t *testing.T) {
233+
catcher := &logCatcher{}
234+
l := &Logger{client: catcher}
235+
bl := NewBufferedLogger(l)
236+
237+
message := []byte("error message\n")
238+
_, err := bl.Write(message)
239+
if err != nil {
240+
t.Fatalf("unexpected write error: %v", err)
241+
}
242+
243+
originalErr := errors.New("command failed")
244+
if gotErr := bl.Flush(ctx, originalErr); gotErr != originalErr {
245+
t.Errorf("Flush(ctx, err) returned %v, want %v", gotErr, originalErr)
246+
}
247+
248+
received := catcher.msgs[0].GetLogEntries()[0]
249+
if got, want := received.Message, "error message"; got != want {
250+
t.Errorf("got message %q, want %q", got, want)
251+
}
252+
if got, want := received.Severity, fnpb.LogEntry_Severity_ERROR; got != want {
253+
t.Errorf("got severity %v, want %v", got, want)
254+
}
255+
})
256+
189257
t.Run("direct print", func(t *testing.T) {
190258
catcher := &logCatcher{}
191259
l := &Logger{client: catcher}

sdks/python/container/boot.go

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package main
1919

2020
import (
21-
"bytes"
2221
"context"
2322
"encoding/json"
2423
"errors"
@@ -572,37 +571,27 @@ func logRuntimeDependencies(ctx context.Context, bufLogger *tools.BufferedLogger
572571
if err != nil {
573572
return err
574573
}
575-
bufLogger.Printf(ctx, "Python version in %s:", phase)
576-
args := []string{"--version"}
577-
if err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...); err != nil {
578-
bufLogger.FlushAtError(ctx)
579-
} else {
580-
bufLogger.FlushAtDebug(ctx)
574+
if out, err := executeWithOutput(ctx, bufLogger, pythonVersion, "--version"); err == nil {
575+
bufLogger.Printf(ctx, "Python version in %s: %s", phase, strings.TrimSpace(string(out)))
581576
}
582-
bufLogger.Printf(ctx, "Dependencies in %s:", phase)
583-
args = []string{"-m", "pip", "freeze", "--all"}
584577

585-
var stdout bytes.Buffer
586-
if err := execx.ExecuteEnvWithIO(nil, os.Stdin, &stdout, bufLogger, pythonVersion, args...); err != nil {
587-
bufLogger.FlushAtError(ctx)
588-
} else {
589-
bufLogger.FlushAtDebug(ctx)
590-
bufLogger.Printf(ctx, "%s", stdout.String())
578+
args := []string{"-m", "pip", "freeze", "--all"}
579+
if out, err := executeWithOutput(ctx, bufLogger, pythonVersion, args...); err == nil {
580+
bufLogger.Printf(ctx, "Dependencies in %s:\n%s", phase, string(out))
591581
}
592582
return nil
593583
}
594584

595585
// logSubmissionEnvDependencies logs the python dependencies
596586
// installed in the submission environment.
597587
func logSubmissionEnvDependencies(ctx context.Context, bufLogger *tools.BufferedLogger, dir string) error {
598-
bufLogger.Printf(ctx, "Dependencies in submission environment:")
599588
// path for submission environment dependencies should match with the
600589
// one defined in apache_beam/runners/portability/stager.py.
601590
filename := filepath.Join(dir, "submission_environment_dependencies.txt")
602591
content, err := os.ReadFile(filename)
603592
if err != nil {
604593
return err
605594
}
606-
bufLogger.Printf(ctx, "%s", string(content))
595+
bufLogger.Printf(ctx, "Dependencies in submission environment:\n%s", string(content))
607596
return nil
608597
}

sdks/python/container/piputil.go

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ var (
4141
const pipLogFlushInterval time.Duration = 15 * time.Second
4242
const unrecoverableURL string = "https://beam.apache.org/documentation/sdks/python-unrecoverable-errors/index.html#pip-dependency-resolution-failures"
4343

44+
// executeWithLogger runs the program with os.Stdin, piping stdout and stderr to bufLogger,
45+
// and flushes bufLogger based on the execution result.
46+
func executeWithLogger(ctx context.Context, bufLogger *tools.BufferedLogger, prog string, args ...string) error {
47+
err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, prog, args...)
48+
return bufLogger.Flush(ctx, err)
49+
}
50+
51+
// executeWithOutput runs the program with os.Stdin, capturing stdout in a byte buffer
52+
// while piping stderr to bufLogger, and flushes bufLogger based on the execution result.
53+
func executeWithOutput(ctx context.Context, bufLogger *tools.BufferedLogger, prog string, args ...string) ([]byte, error) {
54+
var stdout bytes.Buffer
55+
err := execx.ExecuteEnvWithIO(nil, os.Stdin, &stdout, bufLogger, prog, args...)
56+
if flushErr := bufLogger.Flush(ctx, err); flushErr != nil {
57+
return nil, flushErr
58+
}
59+
return stdout.Bytes(), nil
60+
}
61+
4462
// pipInstallRequirements installs the given requirement, if present.
4563
func pipInstallRequirements(ctx context.Context, logger *tools.Logger, files []string, dir, name string) error {
4664
pythonVersion, err := expansionx.GetPythonVersion()
@@ -62,12 +80,9 @@ func pipInstallRequirements(ctx context.Context, logger *tools.Logger, files []s
6280
// also installs dependencies. The key is that if all the packages have
6381
// been installed in the first round then this command will be a no-op.
6482
args = []string{"-m", "pip", "install", "-r", filepath.Join(dir, name), "--no-cache-dir", "--disable-pip-version-check", "--find-links", dir}
65-
err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...)
66-
if err != nil {
67-
bufLogger.FlushAtError(ctx)
83+
if err := executeWithLogger(ctx, bufLogger, pythonVersion, args...); err != nil {
6884
return fmt.Errorf("PIP failed to install dependencies, got %s. This error may be unrecoverable, see %s for more information", err, unrecoverableURL)
6985
}
70-
bufLogger.FlushAtDebug(ctx)
7186
return nil
7287
}
7388
}
@@ -121,23 +136,16 @@ func pipInstallPackage(ctx context.Context, logger *tools.Logger, files []string
121136
if pipNoBuildIsolation {
122137
args = append(args, "--no-build-isolation")
123138
}
124-
err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...)
125-
if err != nil {
126-
bufLogger.FlushAtError(ctx)
139+
if err := executeWithLogger(ctx, bufLogger, pythonVersion, args...); err != nil {
127140
return fmt.Errorf("PIP failed to install dependencies, got %s. This error may be unrecoverable, see %s for more information", err, unrecoverableURL)
128-
} else {
129-
bufLogger.FlushAtDebug(ctx)
130141
}
131142
args = []string{"-m", "pip", "install", "--no-cache-dir", "--disable-pip-version-check", filepath.Join(dir, packageSpec)}
132143
if pipNoBuildIsolation {
133144
args = append(args, "--no-build-isolation")
134145
}
135-
err = execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...)
136-
if err != nil {
137-
bufLogger.FlushAtError(ctx)
146+
if err := executeWithLogger(ctx, bufLogger, pythonVersion, args...); err != nil {
138147
return fmt.Errorf("PIP failed to install dependencies, got %s. This error may be unrecoverable, see %s for more information", err, unrecoverableURL)
139148
}
140-
bufLogger.FlushAtDebug(ctx)
141149
return nil
142150
}
143151

@@ -146,12 +154,9 @@ func pipInstallPackage(ctx context.Context, logger *tools.Logger, files []string
146154
if pipNoBuildIsolation {
147155
args = append(args, "--no-build-isolation")
148156
}
149-
err := execx.ExecuteEnvWithIO(nil, os.Stdin, bufLogger, bufLogger, pythonVersion, args...)
150-
if err != nil {
151-
bufLogger.FlushAtError(ctx)
157+
if err := executeWithLogger(ctx, bufLogger, pythonVersion, args...); err != nil {
152158
return fmt.Errorf("PIP failed to install dependencies, got %s. This error may be unrecoverable, see %s for more information", err, unrecoverableURL)
153159
}
154-
bufLogger.FlushAtDebug(ctx)
155160
return nil
156161
}
157162
}

0 commit comments

Comments
 (0)