diff --git a/README.md b/README.md index 9e2bc3888..7be1870b5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ plain text): With `log.SetFormatter(&log.JSONFormatter{})`, for easy parsing by logstash or Splunk: -```json +```text {"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"} @@ -384,7 +384,7 @@ Third party logging formatters: * [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout. * [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the Power of Zalgo. * [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure. -* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Sava log to files. +* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Save log to files. * [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added. You can define your formatter by implementing the `Formatter` interface, diff --git a/entry_test.go b/entry_test.go index 41c47a2fb..d70266104 100644 --- a/entry_test.go +++ b/entry_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/assert" ) +type contextKeyType string + func TestEntryWithError(t *testing.T) { assert := assert.New(t) @@ -36,7 +38,8 @@ func TestEntryWithError(t *testing.T) { func TestEntryWithContext(t *testing.T) { assert := assert.New(t) - ctx := context.WithValue(context.Background(), "foo", "bar") + var contextKey contextKeyType = "foo" + ctx := context.WithValue(context.Background(), contextKey, "bar") assert.Equal(ctx, WithContext(ctx).Context) @@ -56,11 +59,13 @@ func TestEntryWithContextCopiesData(t *testing.T) { parentEntry := NewEntry(logger).WithField("parentKey", "parentValue") // Create two children Entry objects from the parent in different contexts - ctx1 := context.WithValue(context.Background(), "foo", "bar") + var contextKey1 contextKeyType = "foo" + ctx1 := context.WithValue(context.Background(), contextKey1, "bar") childEntry1 := parentEntry.WithContext(ctx1) assert.Equal(ctx1, childEntry1.Context) - ctx2 := context.WithValue(context.Background(), "bar", "baz") + var contextKey2 contextKeyType = "bar" + ctx2 := context.WithValue(context.Background(), contextKey2, "baz") childEntry2 := parentEntry.WithContext(ctx2) assert.Equal(ctx2, childEntry2.Context) assert.NotEqual(ctx1, ctx2) diff --git a/logrus_test.go b/logrus_test.go index 4edee2834..72a02de1e 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -544,8 +544,7 @@ func TestParseLevel(t *testing.T) { } func TestLevelString(t *testing.T) { - var loggerlevel Level - loggerlevel = 32000 + var loggerlevel Level = 32000 _ = loggerlevel.String() } diff --git a/terminal_check_bsd.go b/terminal_check_bsd.go index 499789984..69956b425 100644 --- a/terminal_check_bsd.go +++ b/terminal_check_bsd.go @@ -1,4 +1,4 @@ -// +build darwin dragonfly freebsd netbsd openbsd +// +build darwin dragonfly freebsd netbsd openbsd hurd // +build !js package logrus diff --git a/terminal_check_unix.go b/terminal_check_unix.go index 04748b851..c9aed267a 100644 --- a/terminal_check_unix.go +++ b/terminal_check_unix.go @@ -1,5 +1,7 @@ +//go:build (linux || aix || zos) && !js && !wasi // +build linux aix zos // +build !js +// +build !wasi package logrus diff --git a/terminal_check_wasi.go b/terminal_check_wasi.go new file mode 100644 index 000000000..2822b212f --- /dev/null +++ b/terminal_check_wasi.go @@ -0,0 +1,8 @@ +//go:build wasi +// +build wasi + +package logrus + +func isTerminal(fd int) bool { + return false +} diff --git a/terminal_check_wasip1.go b/terminal_check_wasip1.go new file mode 100644 index 000000000..108a6be12 --- /dev/null +++ b/terminal_check_wasip1.go @@ -0,0 +1,8 @@ +//go:build wasip1 +// +build wasip1 + +package logrus + +func isTerminal(fd int) bool { + return false +} diff --git a/writer.go b/writer.go index 72e8e3a1b..074fd4b8b 100644 --- a/writer.go +++ b/writer.go @@ -4,6 +4,7 @@ import ( "bufio" "io" "runtime" + "strings" ) // Writer at INFO level. See WriterLevel for details. @@ -20,15 +21,18 @@ func (logger *Logger) WriterLevel(level Level) *io.PipeWriter { return NewEntry(logger).WriterLevel(level) } +// Writer returns an io.Writer that writes to the logger at the info log level func (entry *Entry) Writer() *io.PipeWriter { return entry.WriterLevel(InfoLevel) } +// WriterLevel returns an io.Writer that writes to the logger at the given log level func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { reader, writer := io.Pipe() var printFunc func(args ...interface{}) + // Determine which log function to use based on the specified log level switch level { case TraceLevel: printFunc = entry.Trace @@ -48,23 +52,51 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter { printFunc = entry.Print } + // Start a new goroutine to scan the input and write it to the logger using the specified print function. + // It splits the input into chunks of up to 64KB to avoid buffer overflows. go entry.writerScanner(reader, printFunc) + + // Set a finalizer function to close the writer when it is garbage collected runtime.SetFinalizer(writer, writerFinalizer) return writer } +// writerScanner scans the input from the reader and writes it to the logger func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) { scanner := bufio.NewScanner(reader) + + // Set the buffer size to the maximum token size to avoid buffer overflows + scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), bufio.MaxScanTokenSize) + + // Define a split function to split the input into chunks of up to 64KB + chunkSize := bufio.MaxScanTokenSize // 64KB + splitFunc := func(data []byte, atEOF bool) (int, []byte, error) { + if len(data) >= chunkSize { + return chunkSize, data[:chunkSize], nil + } + + return bufio.ScanLines(data, atEOF) + } + + // Use the custom split function to split the input + scanner.Split(splitFunc) + + // Scan the input and write it to the logger using the specified print function for scanner.Scan() { - printFunc(scanner.Text()) + printFunc(strings.TrimRight(scanner.Text(), "\r\n")) } + + // If there was an error while scanning the input, log an error if err := scanner.Err(); err != nil { entry.Errorf("Error while reading from Writer: %s", err) } + + // Close the reader when we are done reader.Close() } +// WriterFinalizer is a finalizer function that closes then given writer when it is garbage collected func writerFinalizer(writer *io.PipeWriter) { writer.Close() } diff --git a/writer_test.go b/writer_test.go index 5c34927da..5b6261bd1 100644 --- a/writer_test.go +++ b/writer_test.go @@ -1,10 +1,16 @@ package logrus_test import ( + "bufio" + "bytes" "log" "net/http" + "strings" + "testing" + "time" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) func ExampleLogger_Writer_httpServer() { @@ -32,3 +38,61 @@ func ExampleLogger_Writer_stdlib() { // Not logrus imported under the name `log`. log.SetOutput(logger.Writer()) } + +func TestWriterSplitNewlines(t *testing.T) { + buf := bytes.NewBuffer(nil) + logger := logrus.New() + logger.Formatter = &logrus.TextFormatter{ + DisableColors: true, + DisableTimestamp: true, + } + logger.SetOutput(buf) + writer := logger.Writer() + + const logNum = 10 + + for i := 0; i < logNum; i++ { + _, err := writer.Write([]byte("bar\nfoo\n")) + assert.NoError(t, err, "writer.Write failed") + } + writer.Close() + // Test is flaky because it writes in another goroutine, + // we need to make sure to wait a bit so all write are done. + time.Sleep(500 * time.Millisecond) + + lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") + assert.Len(t, lines, logNum*2, "logger printed incorrect number of lines") +} + +func TestWriterSplitsMax64KB(t *testing.T) { + buf := bytes.NewBuffer(nil) + logger := logrus.New() + logger.Formatter = &logrus.TextFormatter{ + DisableColors: true, + DisableTimestamp: true, + } + logger.SetOutput(buf) + writer := logger.Writer() + + // write more than 64KB + const bigWriteLen = bufio.MaxScanTokenSize + 100 + output := make([]byte, bigWriteLen) + // lets not write zero bytes + for i := 0; i < bigWriteLen; i++ { + output[i] = 'A' + } + + for i := 0; i < 3; i++ { + len, err := writer.Write(output) + assert.NoError(t, err, "writer.Write failed") + assert.Equal(t, bigWriteLen, len, "bytes written") + } + writer.Close() + // Test is flaky because it writes in another goroutine, + // we need to make sure to wait a bit so all write are done. + time.Sleep(500 * time.Millisecond) + + lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") + // we should have 4 lines because we wrote more than 64 KB each time + assert.Len(t, lines, 4, "logger printed incorrect number of lines") +}