Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 17 additions & 61 deletions galog_stderr.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,37 @@
package galog

import (
"context"
"fmt"
"io"
"os"
)

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

// StderrBackend is a simple backend implementation for logging to stderr.
type StderrBackend struct {
// backendID is the internal id of this backend.
backendID string
// config is a pointer to the generic Config interface implementation.
config *backendConfig
// writer by default it's set to use os.Stderr, tests might override it to a
// local writer.
writer io.Writer
*writerBackend
}

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

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

return res
}

// ID returns the stderr backend implementation's ID.
func (wb *StderrBackend) ID() string {
return wb.backendID
}

// Log prints the log entry to stderr.
func (wb *StderrBackend) Log(entry *LogEntry) error {
if entry.Level != ErrorLevel {
return nil
}

format := wb.config.Format(entry.Level)

message, err := entry.Format(format + "\n")
if err != nil {
return fmt.Errorf("failed to format log level: %+v", err)
}

n, err := wb.writer.Write([]byte(message))
if err != nil {
return fmt.Errorf("failed to write log to stderr: %+v", err)
}

if n != len(message) {
return fmt.Errorf("failed to write the message, wrote %d bytes out of %d bytes", n, len(message))
}

return nil
}

// Config returns the backend configuration of the stderr backend.
func (wb *StderrBackend) Config() Config {
return wb.config
}

// Shutdown flushes the stderr file.
func (wb *StderrBackend) Shutdown(context.Context) error {
if err := os.Stderr.Sync(); err != nil {
return fmt.Errorf("failed to flush stderr: %+v", err)
}
return nil
}
18 changes: 14 additions & 4 deletions galog_stderr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ func (ew errorWriter) Write(data []byte) (int, error) {

func TestStderrWriteFailure(t *testing.T) {
logBuffer := &errorWriter{failureType: writeFailure}
be := NewStderrBackend(logBuffer)
be := NewStderrBackend()
be.writer = logBuffer

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

func TestStderrWriteLenFailure(t *testing.T) {
logBuffer := &errorWriter{failureType: writeLenFailure}
be := NewStderrBackend(logBuffer)
be := NewStderrBackend()
be.writer = logBuffer

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

func TestStderrInvalidFormat(t *testing.T) {
logBuffer := bytes.NewBuffer(nil)
be := NewStderrBackend(logBuffer)
be := NewStderrBackend()
be.writer = logBuffer

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

Expand All @@ -88,6 +91,12 @@ func TestStderrSuccess(t *testing.T) {
level: ErrorLevel,
want: "[ERROR]: foo bar\n",
},
{
desc: "fatal_level",
message: "foo bar",
level: FatalLevel,
want: "[FATAL]: foo bar\n",
},
{
desc: "warning_level_skip",
message: "foo bar",
Expand All @@ -111,7 +120,8 @@ func TestStderrSuccess(t *testing.T) {
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
logBuffer := bytes.NewBuffer(nil)
be := NewStderrBackend(logBuffer)
be := NewStderrBackend()
be.writer = logBuffer
if be.Config() == nil {
t.Fatal("NewStderrBackend() failed: Config() returned nil")
}
Expand Down
54 changes: 54 additions & 0 deletions galog_stdout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2026 Google LLC
//
// 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 galog

import (
"os"
)

const (
// defaultStdoutQueueSize defines the default queue size of the stdout
// backend implementation. In general writing to stdout doesn't require
// caching or queueing, we set a limit to avoid the queue to grow
// indefinitely in case of any disastrous behavior of the OS - as 0 means
// "grow indefinitely".
defaultStdoutQueueSize = 10
)

// StdoutBackend is a simple backend implementation for logging to stdout.
type StdoutBackend struct {
*writerBackend
}

// NewStdoutBackend returns a Backend implementation that will log out to the
// process' stdout.
func NewStdoutBackend() *StdoutBackend {
res := &StdoutBackend{
writerBackend: &writerBackend{
backendID: "log-backend,stdout",
config: newBackendConfig(defaultStdoutQueueSize),
writer: os.Stdout,
skip: func(lvl Level) bool { return lvl == ErrorLevel || lvl == FatalLevel },
sync: os.Stdout.Sync,
},
}

res.config.SetFormat(InfoLevel,
`{{.When.Format "2006-01-02T15:04:05.0000Z07:00"}} {{if .Prefix}} {{.Prefix}}: {{end}}[{{.Level}}]: {{.Message}}`)
res.config.SetFormat(DebugLevel,
`{{.When.Format "2006-01-02T15:04:05.0000Z07:00"}} {{if .Prefix}} {{.Prefix}}: {{end}}[{{.Level}}]: ({{.File}}:{{.Line}}) {{.Message}}`)

return res
}
152 changes: 152 additions & 0 deletions galog_stdout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2026 Google LLC
//
// 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 galog

import (
"bytes"
"strings"
"testing"
"time"
)

// helper1 and newTestEntry together wrapper functions to simulate a stack
// depth of 3 layers between the test code calling newTestEntry and the
// runtime.Caller(3) call site inside newEntry.
func helper1(level Level, prefix string, msg string) *LogEntry {
return newEntry(level, prefix, msg)
}

func newTestEntry(level Level, prefix string, msg string) *LogEntry {
return helper1(level, prefix, msg)
}

func TestStdoutWriteFailure(t *testing.T) {
logBuffer := &errorWriter{failureType: writeFailure}
be := NewStdoutBackend()
be.writer = logBuffer

entry := newTestEntry(InfoLevel, "", "foobar")
if err := be.Log(entry); err == nil {
t.Fatalf("Log(%+v) succeeded, want error due to write failure", entry)
}
}

func TestStdoutWriteLenFailure(t *testing.T) {
logBuffer := &errorWriter{failureType: writeLenFailure}
be := NewStdoutBackend()
be.writer = logBuffer

entry := newTestEntry(InfoLevel, "", "foobar")
if err := be.Log(entry); err == nil {
t.Fatalf("Log(%+v) succeeded, want error due to write len failure", entry)
}
}

func TestStdoutInvalidFormat(t *testing.T) {
logBuffer := bytes.NewBuffer(nil)
be := NewStdoutBackend()
be.writer = logBuffer

be.Config().SetFormat(InfoLevel, "{{.Foobar}}")

entry := newTestEntry(InfoLevel, "", "foobar")
if err := be.Log(entry); err == nil {
t.Fatalf("Log(%+v) succeeded, want error due to invalid format", entry)
}
}

func TestStdoutSuccess(t *testing.T) {
tests := []struct {
desc string
message string
level Level
prefix string
want string
}{
{
desc: "error_level_skip",
message: "foo bar",
level: ErrorLevel,
want: "",
},
{
desc: "fatal_level_skip",
message: "foo bar",
level: FatalLevel,
want: "",
},
{
desc: "warning_level",
message: "foo bar",
level: WarningLevel,
want: "[WARNING]: foo bar\n",
},
{
desc: "info_level",
message: "foo bar",
level: InfoLevel,
want: "[INFO]: foo bar\n",
},
{
desc: "info_level_with_prefix",
message: "foo bar",
level: InfoLevel,
prefix: "my-prefix",
want: " my-prefix: [INFO]: foo bar\n",
},
{
desc: "debug_level",
message: "foo bar",
level: DebugLevel,
want: "[DEBUG]:",
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
logBuffer := bytes.NewBuffer(nil)
be := NewStdoutBackend()
be.writer = logBuffer
if be.Config() == nil {
t.Fatal("NewStdoutBackend() failed: Config() returned nil")
}

entry := newTestEntry(tc.level, tc.prefix, tc.message)
if err := be.Log(entry); err != nil {
t.Fatalf("Log(%+v) failed: %v", entry, err)
}

got := logBuffer.String()

if tc.want == "" {
if logBuffer.Len() != 0 {
t.Fatalf("Log(%+v) output = %q, want empty", entry, got)
}
return
}

if tc.level == DebugLevel {
if !strings.Contains(got, "[DEBUG]:") || !strings.Contains(got, "galog_stdout_test.go:") || !strings.HasSuffix(got, tc.message+"\n") {
t.Fatalf("Log(%+v) output = %q, want [DEBUG]:, galog_stdout_test.go:, and suffix %q", entry, got, tc.message+"\n")
}
} else {
if !strings.HasSuffix(got, tc.want) {
t.Fatalf("Log(%+v) output = %q, want suffix %q", entry, got, tc.want)
}
}
Shutdown(time.Millisecond)
})
}
}
Loading