TUN-3767: Tolerate logging errors

This addresses a bug where logging would not be output when
cloudflared was run as a Windows Service.

That was happening because Windows Services have no stderr/out,
and the ConsoleWriter log was failing inside zerolog, which would
then not proceed to the next logger (the file logger).

We now overcome that by using our own multi writer that is resilient
to errors.
This commit is contained in:
Nuno Diegues 2021-01-16 15:56:44 +00:00
parent 8da61274b8
commit de27361ffa
2 changed files with 105 additions and 1 deletions

View File

@ -37,6 +37,21 @@ func fallbackLogger(err error) *zerolog.Logger {
return &failLog return &failLog
} }
type resilientMultiWriter struct {
writers []io.Writer
}
// This custom resilientMultiWriter is an alternative to zerolog's so that we can make it resilient to individual
// writer's errors. E.g., when running as a Windows service, the console writer fails, but we don't want to
// allow that to prevent all logging to fail due to breaking the for loop upon an error.
func (t resilientMultiWriter) Write(p []byte) (n int, err error) {
for _, w := range t.writers {
_, _ = w.Write(p)
}
return len(p), nil
}
func newZerolog(loggerConfig *Config) *zerolog.Logger { func newZerolog(loggerConfig *Config) *zerolog.Logger {
var writers []io.Writer var writers []io.Writer
@ -62,7 +77,7 @@ func newZerolog(loggerConfig *Config) *zerolog.Logger {
writers = append(writers, rollingLogger) writers = append(writers, rollingLogger)
} }
multi := zerolog.MultiLevelWriter(writers...) multi := resilientMultiWriter{writers}
level, err := zerolog.ParseLevel(loggerConfig.MinLevel) level, err := zerolog.ParseLevel(loggerConfig.MinLevel)
if err != nil { if err != nil {

89
logger/create_test.go Normal file
View File

@ -0,0 +1,89 @@
package logger
import (
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"io"
"testing"
)
var writeCalls int
type mockedWriter struct {
wantErr bool
}
func (c mockedWriter) Write(p []byte) (int, error) {
writeCalls++
if c.wantErr {
return -1, errors.New("Expected error")
}
return len(p), nil
}
// Tests that a new writer is only used if it actually works.
func TestResilientMultiWriter(t *testing.T) {
tests := []struct {
name string
writers []io.Writer
}{
{
name: "All valid writers",
writers: []io.Writer{
mockedWriter {
wantErr: false,
},
mockedWriter {
wantErr: false,
},
},
},
{
name: "All invalid writers",
writers: []io.Writer{
mockedWriter {
wantErr: true,
},
mockedWriter {
wantErr: true,
},
},
},
{
name: "First invalid writer",
writers: []io.Writer{
mockedWriter {
wantErr: true,
},
mockedWriter {
wantErr: false,
},
},
},
{
name: "First valid writer",
writers: []io.Writer{
mockedWriter {
wantErr: false,
},
mockedWriter {
wantErr: true,
},
},
},
}
for _, tt := range tests {
writers := tt.writers
multiWriter := resilientMultiWriter{writers}
logger := zerolog.New(multiWriter).With().Timestamp().Logger().Level(zerolog.InfoLevel)
logger.Info().Msg("Test msg")
assert.Equal(t, len(writers), writeCalls)
writeCalls = 0
}
}