Skip to content

Commit 59ef555

Browse files
Refactor galog backends and add stdout backend.
Introduces a shared writerBackend to reduce code duplication between `StderrBackend` and the new `StdoutBackend`. StderrBackend now only logs ErrorLevel messages, while StdoutBackend logs all levels except ErrorLevel. The NewStderrBackend function no longer requires a writer argument. PiperOrigin-RevId: 929995172
1 parent 2cc28a7 commit 59ef555

5 files changed

Lines changed: 318 additions & 65 deletions

File tree

galog_stderr.go

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,37 @@
1515
package galog
1616

1717
import (
18-
"context"
19-
"fmt"
2018
"io"
2119
"os"
2220
)
2321

2422
const (
25-
// defaultStderrQueueSize defines the default queue size of the stderr backend
26-
// implementation. In general writing to stderr doesn't require caching or
27-
// queueing, we are set a limit to avoid the queue to grow indefinitely in
28-
// case of any disastrous behavior of the OS - as 0 means "grow indefinitely".
23+
// defaultStderrQueueSize defines the default queue size of the stderr
24+
// backend implementation. In general writing to stderr doesn't require
25+
// caching or queueing, we set a limit to avoid the queue to grow
26+
// indefinitely in case of any disastrous behavior of the OS - as 0 means
27+
// "grow indefinitely".
2928
defaultStderrQueueSize = 10
3029
)
3130

3231
// StderrBackend is a simple backend implementation for logging to stderr.
3332
type StderrBackend struct {
34-
// backendID is the internal id of this backend.
35-
backendID string
36-
// config is a pointer to the generic Config interface implementation.
37-
config *backendConfig
38-
// writer by default it's set to use os.Stderr, tests might override it to a
39-
// local writer.
40-
writer io.Writer
33+
*writerBackend
4134
}
4235

4336
// NewStderrBackend returns a Backend implementation that will log out to
44-
// the process' stderr.
45-
func NewStderrBackend(writer io.Writer) *StderrBackend {
37+
// the process' stderr. Writers is deprecated and will be removed in the future.
38+
// It is kept for compatibility with users who may have used it in the past.
39+
// It always writes to os.Stderr.
40+
func NewStderrBackend(writers ...io.Writer) *StderrBackend {
4641
res := &StderrBackend{
47-
backendID: "log-backend,stderr",
48-
config: newBackendConfig(defaultStderrQueueSize),
49-
writer: writer,
42+
writerBackend: &writerBackend{
43+
backendID: "log-backend,stderr",
44+
config: newBackendConfig(defaultStderrQueueSize),
45+
writer: os.Stderr,
46+
skip: func(lvl Level) bool { return lvl != ErrorLevel && lvl != FatalLevel },
47+
sync: os.Stderr.Sync,
48+
},
5049
}
5150

5251
res.config.SetFormat(ErrorLevel,
@@ -56,46 +55,3 @@ func NewStderrBackend(writer io.Writer) *StderrBackend {
5655

5756
return res
5857
}
59-
60-
// ID returns the stderr backend implementation's ID.
61-
func (wb *StderrBackend) ID() string {
62-
return wb.backendID
63-
}
64-
65-
// Log prints the log entry to stderr.
66-
func (wb *StderrBackend) Log(entry *LogEntry) error {
67-
if entry.Level != ErrorLevel {
68-
return nil
69-
}
70-
71-
format := wb.config.Format(entry.Level)
72-
73-
message, err := entry.Format(format + "\n")
74-
if err != nil {
75-
return fmt.Errorf("failed to format log level: %+v", err)
76-
}
77-
78-
n, err := wb.writer.Write([]byte(message))
79-
if err != nil {
80-
return fmt.Errorf("failed to write log to stderr: %+v", err)
81-
}
82-
83-
if n != len(message) {
84-
return fmt.Errorf("failed to write the message, wrote %d bytes out of %d bytes", n, len(message))
85-
}
86-
87-
return nil
88-
}
89-
90-
// Config returns the backend configuration of the stderr backend.
91-
func (wb *StderrBackend) Config() Config {
92-
return wb.config
93-
}
94-
95-
// Shutdown flushes the stderr file.
96-
func (wb *StderrBackend) Shutdown(context.Context) error {
97-
if err := os.Stderr.Sync(); err != nil {
98-
return fmt.Errorf("failed to flush stderr: %+v", err)
99-
}
100-
return nil
101-
}

galog_stderr_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ func (ew errorWriter) Write(data []byte) (int, error) {
4242

4343
func TestStderrWriteFailure(t *testing.T) {
4444
logBuffer := &errorWriter{failureType: writeFailure}
45-
be := NewStderrBackend(logBuffer)
45+
be := NewStderrBackend()
46+
be.writer = logBuffer
4647

4748
entry := newEntry(ErrorLevel, "", "foobar")
4849
err := be.Log(entry)
@@ -53,7 +54,8 @@ func TestStderrWriteFailure(t *testing.T) {
5354

5455
func TestStderrWriteLenFailure(t *testing.T) {
5556
logBuffer := &errorWriter{failureType: writeLenFailure}
56-
be := NewStderrBackend(logBuffer)
57+
be := NewStderrBackend()
58+
be.writer = logBuffer
5759

5860
entry := newEntry(ErrorLevel, "", "foobar")
5961
err := be.Log(entry)
@@ -64,7 +66,8 @@ func TestStderrWriteLenFailure(t *testing.T) {
6466

6567
func TestStderrInvalidFormat(t *testing.T) {
6668
logBuffer := bytes.NewBuffer(nil)
67-
be := NewStderrBackend(logBuffer)
69+
be := NewStderrBackend()
70+
be.writer = logBuffer
6871

6972
be.Config().SetFormat(ErrorLevel, "{{.Foobar}}")
7073

@@ -88,6 +91,12 @@ func TestStderrSuccess(t *testing.T) {
8891
level: ErrorLevel,
8992
want: "[ERROR]: foo bar\n",
9093
},
94+
{
95+
desc: "fatal_level",
96+
message: "foo bar",
97+
level: FatalLevel,
98+
want: "[FATAL]: foo bar\n",
99+
},
91100
{
92101
desc: "warning_level_skip",
93102
message: "foo bar",
@@ -111,7 +120,8 @@ func TestStderrSuccess(t *testing.T) {
111120
for _, tc := range tests {
112121
t.Run(tc.desc, func(t *testing.T) {
113122
logBuffer := bytes.NewBuffer(nil)
114-
be := NewStderrBackend(logBuffer)
123+
be := NewStderrBackend()
124+
be.writer = logBuffer
115125
if be.Config() == nil {
116126
t.Fatal("NewStderrBackend() failed: Config() returned nil")
117127
}

galog_stdout.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package galog
16+
17+
import (
18+
"os"
19+
)
20+
21+
const (
22+
// defaultStdoutQueueSize defines the default queue size of the stdout
23+
// backend implementation. In general writing to stdout doesn't require
24+
// caching or queueing, we set a limit to avoid the queue to grow
25+
// indefinitely in case of any disastrous behavior of the OS - as 0 means
26+
// "grow indefinitely".
27+
defaultStdoutQueueSize = 10
28+
)
29+
30+
// StdoutBackend is a simple backend implementation for logging to stdout.
31+
type StdoutBackend struct {
32+
*writerBackend
33+
}
34+
35+
// NewStdoutBackend returns a Backend implementation that will log out to the
36+
// process' stdout.
37+
func NewStdoutBackend() *StdoutBackend {
38+
res := &StdoutBackend{
39+
writerBackend: &writerBackend{
40+
backendID: "log-backend,stdout",
41+
config: newBackendConfig(defaultStdoutQueueSize),
42+
writer: os.Stdout,
43+
skip: func(lvl Level) bool { return lvl == ErrorLevel || lvl == FatalLevel },
44+
sync: os.Stdout.Sync,
45+
},
46+
}
47+
48+
res.config.SetFormat(InfoLevel,
49+
`{{.When.Format "2006-01-02T15:04:05.0000Z07:00"}} {{if .Prefix}} {{.Prefix}}: {{end}}[{{.Level}}]: {{.Message}}`)
50+
res.config.SetFormat(DebugLevel,
51+
`{{.When.Format "2006-01-02T15:04:05.0000Z07:00"}} {{if .Prefix}} {{.Prefix}}: {{end}}[{{.Level}}]: ({{.File}}:{{.Line}}) {{.Message}}`)
52+
53+
return res
54+
}

galog_stdout_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package galog
16+
17+
import (
18+
"bytes"
19+
"strings"
20+
"testing"
21+
"time"
22+
)
23+
24+
// helper1 and newTestEntry together wrapper functions to simulate a stack
25+
// depth of 3 layers between the test code calling newTestEntry and the
26+
// runtime.Caller(3) call site inside newEntry.
27+
func helper1(level Level, prefix string, msg string) *LogEntry {
28+
return newEntry(level, prefix, msg)
29+
}
30+
31+
func newTestEntry(level Level, prefix string, msg string) *LogEntry {
32+
return helper1(level, prefix, msg)
33+
}
34+
35+
func TestStdoutWriteFailure(t *testing.T) {
36+
logBuffer := &errorWriter{failureType: writeFailure}
37+
be := NewStdoutBackend()
38+
be.writer = logBuffer
39+
40+
entry := newTestEntry(InfoLevel, "", "foobar")
41+
if err := be.Log(entry); err == nil {
42+
t.Fatalf("Log(%+v) succeeded, want error due to write failure", entry)
43+
}
44+
}
45+
46+
func TestStdoutWriteLenFailure(t *testing.T) {
47+
logBuffer := &errorWriter{failureType: writeLenFailure}
48+
be := NewStdoutBackend()
49+
be.writer = logBuffer
50+
51+
entry := newTestEntry(InfoLevel, "", "foobar")
52+
if err := be.Log(entry); err == nil {
53+
t.Fatalf("Log(%+v) succeeded, want error due to write len failure", entry)
54+
}
55+
}
56+
57+
func TestStdoutInvalidFormat(t *testing.T) {
58+
logBuffer := bytes.NewBuffer(nil)
59+
be := NewStdoutBackend()
60+
be.writer = logBuffer
61+
62+
be.Config().SetFormat(InfoLevel, "{{.Foobar}}")
63+
64+
entry := newTestEntry(InfoLevel, "", "foobar")
65+
if err := be.Log(entry); err == nil {
66+
t.Fatalf("Log(%+v) succeeded, want error due to invalid format", entry)
67+
}
68+
}
69+
70+
func TestStdoutSuccess(t *testing.T) {
71+
tests := []struct {
72+
desc string
73+
message string
74+
level Level
75+
prefix string
76+
want string
77+
}{
78+
{
79+
desc: "error_level_skip",
80+
message: "foo bar",
81+
level: ErrorLevel,
82+
want: "",
83+
},
84+
{
85+
desc: "fatal_level_skip",
86+
message: "foo bar",
87+
level: FatalLevel,
88+
want: "",
89+
},
90+
{
91+
desc: "warning_level",
92+
message: "foo bar",
93+
level: WarningLevel,
94+
want: "[WARNING]: foo bar\n",
95+
},
96+
{
97+
desc: "info_level",
98+
message: "foo bar",
99+
level: InfoLevel,
100+
want: "[INFO]: foo bar\n",
101+
},
102+
{
103+
desc: "info_level_with_prefix",
104+
message: "foo bar",
105+
level: InfoLevel,
106+
prefix: "my-prefix",
107+
want: " my-prefix: [INFO]: foo bar\n",
108+
},
109+
{
110+
desc: "debug_level",
111+
message: "foo bar",
112+
level: DebugLevel,
113+
want: "[DEBUG]:",
114+
},
115+
}
116+
117+
for _, tc := range tests {
118+
t.Run(tc.desc, func(t *testing.T) {
119+
logBuffer := bytes.NewBuffer(nil)
120+
be := NewStdoutBackend()
121+
be.writer = logBuffer
122+
if be.Config() == nil {
123+
t.Fatal("NewStdoutBackend() failed: Config() returned nil")
124+
}
125+
126+
entry := newTestEntry(tc.level, tc.prefix, tc.message)
127+
if err := be.Log(entry); err != nil {
128+
t.Fatalf("Log(%+v) failed: %v", entry, err)
129+
}
130+
131+
got := logBuffer.String()
132+
133+
if tc.want == "" {
134+
if logBuffer.Len() != 0 {
135+
t.Fatalf("Log(%+v) output = %q, want empty", entry, got)
136+
}
137+
return
138+
}
139+
140+
if tc.level == DebugLevel {
141+
if !strings.Contains(got, "[DEBUG]:") || !strings.Contains(got, "galog_stdout_test.go:") || !strings.HasSuffix(got, tc.message+"\n") {
142+
t.Fatalf("Log(%+v) output = %q, want [DEBUG]:, galog_stdout_test.go:, and suffix %q", entry, got, tc.message+"\n")
143+
}
144+
} else {
145+
if !strings.HasSuffix(got, tc.want) {
146+
t.Fatalf("Log(%+v) output = %q, want suffix %q", entry, got, tc.want)
147+
}
148+
}
149+
Shutdown(time.Millisecond)
150+
})
151+
}
152+
}

0 commit comments

Comments
 (0)