Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
colega authored Aug 9, 2019
2 parents 96c9441 + b02b7a9 commit 4653baa
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

vendor
18 changes: 18 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
language: go

go:
- "1.11.x"
- "1.12.x"

env:
- GO111MODULE=on

install:
- make install

script:
- make test
- make check-fmt

after_success:
- make report-coveralls
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2019-08-09
### Changes
- Initial version
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.PHONY: test help fmt check-fmt install report-coveralls

help: ## Show the help text
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[93m %s\n", $$1, $$2}'

test: ## Run unit tests
@go test -coverprofile=coverage.out -covermode=atomic -race ./...

check-fmt: ## Check file forma
@GOIMP=$$(for f in $$(find . -type f -name "*.go" ! -path "./.cache/*" ! -path "./vendor/*" ! -name "bindata.go") ; do \
goimports -l $$f ; \
done) && echo $$GOIMP && test -z "$$GOIMP"

fmt: ## Format files
@goimports -w $$(find . -name "*.go" -not -path "./vendor/*")

install: ## Installs dependencies
GOPATH=$$GOPATH && go get -u -v \
golang.org/x/tools/cmd/goimports

report-coveralls: ## Reports generated coverage profile to coveralls.io. Intended to be used only from travis
go get github.com/mattn/goveralls && goveralls -coverprofile=coverage.out -service=travis-ci
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# logrusiowriter
## `io.Writer` implementation using logrus

[![Travis CI build status](https://travis-ci.com/cabify/logrusiowriter.svg?branch=master)](https://travis-ci.com/cabify/logrusiowriter)
[![Coverage Status](https://coveralls.io/repos/github/cabify/logrusiowriter/badge.svg)](https://coveralls.io/github/cabify/logrusiowriter)
[![GoDoc](https://godoc.org/github.com/cabify/logrusiowriter?status.svg)](https://godoc.org/github.com/cabify/logrusiowriter)

# Motivation

Many golang libraries use the golang's `log` package to print their logs. This means that if your application
uses logrus to print structured logging, those packages will print a format that is (probably) incompatible with yours,
and you may end losing logs in your logs collector because they can't be parsed properly.

# Solution

Print the logs written using `log.Printf` through `logrus`, by setting `log.SetOutput` to an `io.Writer` implementation
that uses `logrus` as output, i.e.:

```go
log.SetOutput(logrusiowriter.New())
```

See `example_*_test.go` files to find testable examples that serve as documentation.
67 changes: 67 additions & 0 deletions configs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package logrusiowriter

import "github.com/sirupsen/logrus"

// Config holds the configuration to be used with WithConfig() configurer
// This struct is useful to embed into configuration structs parsed with libraries like envconfig
type Config struct {
Level string `default:"info"`
Fields logrus.Fields `default:"logger:stdlib"`
}

// WithLogger configures the logger with the one provided
func WithLogger(logger logrus.FieldLogger) Configurer {
return func(w *writer) {
w.logger = logger
}
}

// WithLevel configures the level with the one provided
func WithLevel(lvl logrus.Level) Configurer {
return func(w *writer) {
w.level = lvl
}
}

// WithFields configures the fields with the ones provided
func WithFields(fields logrus.Fields) Configurer {
return func(w *writer) {
w.fields = fields
}
}

// WithConfig creates a configurer from the configuration provided as a struct
// If it's unable to parse the Level provided as a string, it will invoke the OnLevelParseError function and set the
// level returned by that function (a default value)
func WithConfig(cfg Config) Configurer {
return func(w *writer) {
lvl, err := logrus.ParseLevel(cfg.Level)
if err != nil {
lvl = OnLevelParseError(err)
}
w.level = lvl
w.fields = cfg.Fields
}
}

// WithConfigInterface creates a configurer from the configuration provided as an interface
func WithConfigInterface(cfg interface {
Level() logrus.Level
Fields() logrus.Fields
Logger() logrus.FieldLogger
}) Configurer {
return func(w *writer) {
w.logger = cfg.Logger()
w.level = cfg.Level()
w.fields = cfg.Fields()
}
}

// OnLevelParseError will be invoked if logrus is unable to parse the string level provided in the configuration
// The default behavior is to log it with logrus and return a default Info level,
// you can change this to log in some other system or to panic
// Changing this is not thread safe, so it might be a good idea to change it in a init() function
var OnLevelParseError = func(err error) logrus.Level {
logrus.Errorf("Can't parse level: %s", err)
return logrus.InfoLevel
}
54 changes: 54 additions & 0 deletions configs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package logrusiowriter

import (
"testing"

"github.com/sirupsen/logrus"
)

func TestWithConfig(t *testing.T) {
t.Run("can't parse level, configures info level by default", func(t *testing.T) {
expectedLevel := logrus.InfoLevel

cfg := Config{
Level: "none",
Fields: logrus.Fields{},
}

w := New(WithConfig(cfg))

configuredLevel := w.(*writer).level
if configuredLevel != expectedLevel {
t.Errorf("Configured level should be %s, but it was %s", expectedLevel, configuredLevel)
}
})

t.Run("custom OnLevelParseError", func(t *testing.T) {
originalOnLevelParseError := OnLevelParseError
defer func() { OnLevelParseError = originalOnLevelParseError }()

expectedLevel := logrus.WarnLevel

cfg := Config{
Level: "none",
Fields: logrus.Fields{},
}

var providedErr error
OnLevelParseError = func(err error) logrus.Level {
providedErr = err
return expectedLevel
}

w := New(WithConfig(cfg))

configuredLevel := w.(*writer).level
if configuredLevel != expectedLevel {
t.Errorf("Configured level should be %s, but it was %s", expectedLevel, configuredLevel)
}

if providedErr == nil {
t.Errorf("Error provided to OnLevelParseError should not be nil")
}
})
}
93 changes: 93 additions & 0 deletions example_configs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package logrusiowriter_test

import (
"fmt"

"github.com/cabify/logrusiowriter"
"github.com/sirupsen/logrus"
)

func ExampleWithConfig() {
removeTimestampAndSetOutputToStdout(logrus.StandardLogger())

config := logrusiowriter.Config{
Level: "warning",
Fields: map[string]interface{}{
"config": "struct",
},
}

writer := logrusiowriter.New(
logrusiowriter.WithConfig(config),
)

_, _ = fmt.Fprint(writer, "Hello World!")
// Output:
// level=warning msg="Hello World!" config=struct
}

func ExampleWithFields() {
removeTimestampAndSetOutputToStdout(logrus.StandardLogger())

writer := logrusiowriter.New(
logrusiowriter.WithFields(logrus.Fields{
"config": "fields",
"other": 288,
}),
)

_, _ = fmt.Fprint(writer, "Hello World!")
// Output:
// level=info msg="Hello World!" config=fields other=288
}

func ExampleWithLevel() {
removeTimestampAndSetOutputToStdout(logrus.StandardLogger())

writer := logrusiowriter.New(
logrusiowriter.WithLevel(logrus.ErrorLevel),
)

_, _ = fmt.Fprint(writer, "Hello World!")
// Output:
// level=error msg="Hello World!"
}

func ExampleWithLogger() {
logger := logrus.New()
removeTimestampAndSetOutputToStdout(logger)
logger.SetLevel(logrus.TraceLevel)

writer := logrusiowriter.New(
logrusiowriter.WithLogger(logger),
)

_, _ = fmt.Fprint(writer, "Hello World!")
// Output:
// level=info msg="Hello World!"
}

func ExampleWithConfigInterface() {
removeTimestampAndSetOutputToStdout(logrus.StandardLogger())

writer := logrusiowriter.New(
logrusiowriter.WithConfigInterface(configProvider{}),
)

_, _ = fmt.Fprint(writer, "Hello World!")
// Output:
// level=trace msg="Hello World!" config=interface
}

type configProvider struct{}

func (configProvider) Level() logrus.Level { return logrus.TraceLevel }

func (configProvider) Fields() logrus.Fields { return logrus.Fields{"config": "interface"} }

func (configProvider) Logger() logrus.FieldLogger {
logger := logrus.New()
removeTimestampAndSetOutputToStdout(logger)
logger.SetLevel(logrus.TraceLevel)
return logger
}
20 changes: 20 additions & 0 deletions example_writer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package logrusiowriter_test

import (
"log"

"github.com/cabify/logrusiowriter"
"github.com/sirupsen/logrus"
)

func ExampleNew() {
removeTimestampAndSetOutputToStdout(logrus.StandardLogger())

log.SetOutput(logrusiowriter.New())
log.SetFlags(0) // no date on standard logger

log.Printf("Standard log")

// Output:
// level=info msg="Standard log\n"
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/cabify/logrusiowriter

go 1.12

require (
github.com/sirupsen/logrus v1.4.2
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa // indirect
)
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21 changes: 21 additions & 0 deletions init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package logrusiowriter_test

import (
"os"

"github.com/sirupsen/logrus"
)

var datelessFormatter = new(logrus.TextFormatter)

func init() {
datelessFormatter.DisableTimestamp = true
}

// removeTimestampAndSetOutputToStdout removes date from logrus logs, and redirects them to os.Stdout
// this can't be done in init() because os.Stdout changes after calling init() in examples:
// see: https://unexpected-go.com/os-stdout-changes-after-init-in-examples.html
func removeTimestampAndSetOutputToStdout(logger *logrus.Logger) {
logger.SetFormatter(datelessFormatter)
logger.SetOutput(os.Stdout)
}
Loading

0 comments on commit 4653baa

Please sign in to comment.