Skip to content

Commit 9aed497

Browse files
authored
feat(bazel): add stream_bazel_logs option to tee stderr to os.Stderr (#124)
Adds a per-repository `stream_bazel_logs` config flag (default false). When enabled, Bazel query stderr is tee'd to os.Stderr via io.MultiWriter while still being captured in the existing stderrBuf for error reporting. Threaded through bazel.Params.StreamLogs and wired in nativeOrchestrator from RepositoryConfig.StreamBazelLogs. This way consumers can surface the bazel log as is and know what's going on in the queries live StreamBazelLogs forwards Bazel's stderr to the Tango process's stderr in real time instead of buffering it. When true, operators see query progress live as Bazel runs; the trade-off is that stderr is no longer captured, so query-failure errors will not include it. When false (default) stderr is buffered and folded into the returned error if the query fails.
1 parent 822b3a9 commit 9aed497

4 files changed

Lines changed: 28 additions & 7 deletions

File tree

config/repository_config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type RepositoryConfig struct {
2525
BazelCommand string `yaml:"bazel_command"`
2626
QueryTimeout int64 `yaml:"query_timeout"` // in seconds
2727
BazelExtraArgs []string `yaml:"bazel_extra_args"`
28+
StreamBazelLogs bool `yaml:"stream_bazel_logs"`
2829
}
2930

3031
// RepositoryConfigProvider looks up per-repository configuration by remote.

core/bazel/bazel.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type BazelClient struct {
5656
logger *zap.SugaredLogger
5757
execCommandContext func(ctx context.Context, name string, arg ...string) commander
5858
queryTimeout time.Duration
59+
streamLogs bool
5960
}
6061

6162
type Params struct {
@@ -65,6 +66,7 @@ type Params struct {
6566
Logger *zap.SugaredLogger
6667
ExecCommandContext func(ctx context.Context, name string, arg ...string) commander
6768
QueryTimeout time.Duration
69+
StreamLogs bool
6870
}
6971

7072
func NewBazelClient(ctx context.Context, p Params) (*BazelClient, error) {
@@ -97,6 +99,7 @@ func NewBazelClient(ctx context.Context, p Params) (*BazelClient, error) {
9799
logger: p.Logger,
98100
execCommandContext: execCmd,
99101
queryTimeout: timeout,
102+
streamLogs: p.StreamLogs,
100103
}, nil
101104
}
102105

core/bazel/query.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ func (b *BazelClient) executeQueryInternal(ctx context.Context, query string, st
6767
if err != nil {
6868
return nil, err
6969
}
70+
// In streamLogs mode, stderr goes straight to os.Stderr so operators see
71+
// bazel progress live; otherwise it's captured for inclusion in failure
72+
// errors (see wrapQueryFailure).
73+
stderrSink := io.Writer(&stderrBuf)
74+
if b.streamLogs {
75+
stderrSink = os.Stderr
76+
}
7077
// orchestrate `allOfFailFast`
7178
// create a `g` group and a new `gCtx` derived from our 15 minute timeout `ctx`.
7279
g, gCtx := errgroup.WithContext(cmdCtx)
@@ -83,24 +90,33 @@ func (b *BazelClient) executeQueryInternal(ctx context.Context, query string, st
8390
})
8491
// stream stderr
8592
g.Go(func() error {
86-
return streamOutput(gCtx, stderr, &stderrBuf)
93+
return streamOutput(gCtx, stderr, stderrSink)
8794
})
8895
waitErr := cmd.Wait()
8996
streamErr := g.Wait()
90-
// The command itself failed.
9197
if waitErr != nil {
92-
b.logger.Errorf("Bazel query failed: %v\nstderr:\n%s", waitErr, stderrBuf.String())
93-
return queryResults, fmt.Errorf("bazel query failed: %w\nstderr:\n%s", waitErr, stderrBuf.String())
98+
return queryResults, b.wrapQueryFailure("bazel query failed", waitErr, &stderrBuf)
9499
}
95-
// The command succeeded, but there was an error in the stream processing.
96100
if streamErr != nil {
97-
b.logger.Errorf("Error in stream processing: %v\nstderr:\n%s", streamErr, stderrBuf.String())
98-
return nil, fmt.Errorf("stream processing failed: %w\nstderr:\n%s", streamErr, stderrBuf.String())
101+
return nil, b.wrapQueryFailure("stream processing failed", streamErr, &stderrBuf)
99102
}
100103
b.logger.Debugf("Parsed %d targets from bazel query", len(queryResults.Target))
101104
return queryResults, nil
102105
}
103106

107+
// wrapQueryFailure logs the failure and returns a wrapped error. When stderr
108+
// was captured (streamLogs off), its contents are appended so the failure is
109+
// self-contained. When streamLogs is on the operator has already seen stderr
110+
// live, so it's omitted.
111+
func (b *BazelClient) wrapQueryFailure(msg string, cause error, stderrBuf *bytes.Buffer) error {
112+
tail := ""
113+
if !b.streamLogs {
114+
tail = "\nstderr:\n" + stderrBuf.String()
115+
}
116+
b.logger.Errorf("%s: %v%s", msg, cause, tail)
117+
return fmt.Errorf("%s: %w%s", msg, cause, tail)
118+
}
119+
104120
// FromFile reads a proto file generated by bazel query.
105121
func FromFile(path string) (*buildpb.QueryResult, error) {
106122
var f io.ReadCloser

orchestrator/native_orchestrator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ func (b *nativeOrchestrator) GetTargetGraph(ctx context.Context, param GetTarget
196196
Logger: b.logger,
197197
BazelCommand: repoCfg.BazelCommand,
198198
QueryTimeout: time.Duration(repoCfg.QueryTimeout) * time.Second,
199+
StreamLogs: repoCfg.StreamBazelLogs,
199200
})
200201
if err != nil {
201202
logger.Errorw("GetTargetGraph: Error creating bazel client", zap.Error(err))

0 commit comments

Comments
 (0)