Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored zerolog.ConsoleWriter to allow customization #92

Merged
merged 5 commits into from
Nov 5, 2018

Conversation

karmi
Copy link
Contributor

@karmi karmi commented Jul 28, 2018

As outlined in #84, this patch refactors the zerolog.ConsoleWriter in two ways:

  • The default formatting has been changed to be more sparse and visually attractive
  • The formatting logic has been extracted to separate functions, so it can be changed from the user code

I had to change the design of the prototype in order to maintain the backwards compatibility: the prototype has defined default formatting functions (formatters) on the concrete instance of ConsoleWriter, but without using an initializer, these would be empty. Therefore, I've added a number of defensive checks into ConsoleWriter.Write(), and a ConsoleWriter.Reset() method. This is not ideal, but I agree that maintaining backwards compatibility is an important goal, and the behaviour is predictable in my testing.

I've added a benchmark test to make sure I inadverently don't harm the performance: the performance has stayed roughly the same. The tests and examples have been run with the -race parameter.

I've updated the screenshot to reflect the new look, and added a comprehensive example to the README.

@karmi
Copy link
Contributor Author

karmi commented Jul 28, 2018

The TravisCI build failed with could not write event: cannot decode event: invalid character '¿' looking for beginning of value--- FAIL: TestConsoleLogger, which is surprising, since I can't reproduce it locally — the test runs fine bothin isolation as well as part of the whole suite...

@rs
Copy link
Owner

rs commented Jul 30, 2018

Please use this screenshot:
pretty

@karmi
Copy link
Contributor Author

karmi commented Jul 30, 2018

Hello, screenshot updated, commit amended & force-pushed.

@rs
Copy link
Owner

rs commented Jul 30, 2018

This travis issue is meh. Any clue?

@karmi
Copy link
Contributor Author

karmi commented Jul 30, 2018

@rs, unfortunately no clue... :/ I've run both of the commands in .travis.yml locally, and it runs just fine. What I wanna try is to run the tests inside a Docker container, to replicate the Travis environment more closely. I'll let you know when I do that.

@karmi
Copy link
Contributor Author

karmi commented Jul 30, 2018

I've found it — the call to decodeIfBinaryToBytes() was missing from the new Write() method, and that was (mysteriously) failing on Travis, but not locally. (Any idea why?)

I've squashed the commit into the main one and pushed the new branch.

@rs
Copy link
Owner

rs commented Jul 30, 2018

Oh ok make sense. It’s due to the second line in the script section of the travis file with the tag to test binary logging.

@karmi
Copy link
Contributor Author

karmi commented Jul 30, 2018

Ah, right! Maybe I forgot to check the output of the second test script properly when running it locally.

@karmi
Copy link
Contributor Author

karmi commented Jul 30, 2018

I've just checked again, and indeed, when I run go test -v -tags binary_log -race ./... locally, it does fail without the decodeIfBinaryToBytes() call — sorry about that!

README.md Outdated
To customize the configuration and formatting:

```go
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}.Reset()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not found of this API with the need to call Reset and SetFormatter updating a global config. Why can't we store the formatter settings in the ConsoleWriter type?

@karmi
Copy link
Contributor Author

karmi commented Aug 6, 2018

I don't like it much either :)

I haven't found a better way to keep 100% backwards compatibility with existing user code using zerolog.ConsoleWriter. As I said, in the github.com/karmi/consolelog prototype, I've used an "initializer" function to create a new ConsoleWriter, which is passed to zerolog.New():

output := consolelog.NewConsoleWriter()
logger := zerolog.New(output).With().Timestamp().Logger()

In this way, when a user would customize the consolelog.ConsoleWriter, like this:

output = consolelog.NewConsoleWriter(
		func(w *consolelog.ConsoleWriter) {
			w.TimeFormat = time.Stamp
			w.SetFormatter(
				zerolog.LevelFieldName,
				func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-5s", i)) })			
		},
	)

it would register the customization only for the particular object — in other words, when you would then create another output with the bare NewConsoleWriter() function, it would have the default formatting.

In the prototype, all the formatters, the time format, and the parts order have been indeed stored as properties of ConsoleWriter; https://github.com/karmi/consolelog/blob/cf60d6a7b0da4c2bd928702aaaee4aeef0f093e5/consolelog.go#L44-L49.

But since I imagine users have existing code where the only "initialization" they do is something like log.Output(zerolog.ConsoleWriter{Out: os.Stderr}), I didn't want to break backwards compatibility here, and force them to update the code to switch the literal initialization to the initializer function...

After a lot of thinking and fiddling, I've realized that I can export a Reset() method, which would re-initialize the package variables for any new ConsoleWriter, mirroring the init() logic.

Maybe there's a better way around this? Or maybe I'm over-thinking the backwards compatibility too much?

(Sorry for a delay with the reply, I'm on a vacation.)

@rs
Copy link
Owner

rs commented Aug 7, 2018

I'm not sure the initializer function design is indicated here. We don't you just expose the SetFormatter function and make sure the default formatters (when not set) are the sensible defaults?

@rs
Copy link
Owner

rs commented Aug 31, 2018

@karmi: are you still willing to work on this PR?

@karmi
Copy link
Contributor Author

karmi commented Sep 2, 2018

Yes, absolutely — I was offline for most of August.

[Why] don't you just expose the SetFormatter function and make sure the default formatters (when not set) are the sensible defaults?

You mean as the package level function, or on the ConsoleWriter object, as it is now? Again, maybe I'm missing something, but what I wanted to achieve was sensible defaults which would not override any "bare" initialization of another console writer the user might do — which has led me to the Reset() method...

I'll have another look on the codebase, maybe another solution will jump out at me.

@rs rs force-pushed the master branch 4 times, most recently from 1cea9c5 to 8479412 Compare September 17, 2018 17:28
In order to allow customizing the console output, the `zerolog.ConsoleWriter` output
has been refactored, with the formatting logic extracted to separate functions in `consoleFormatters`,
and with the `SetFormatter()` method to re-define the defaults.

Also, the default has been changed to be more sparse and visually attractive.

The logic has been extracted from an external prototype package at <https://github.com/karmi/consolelog>.

A comprehensive example has been added to the README, and a number of tests has been added as well.

Closes rs#84
karmi added a commit to karmi/zerolog that referenced this pull request Oct 30, 2018
* Removed the global registry (map) of default formatters
* Therefore, removed the `init()` function
* Therefore, removed the `Reset()` method
* Added default formatters as package level variables (`consoleDefaultFormatTimestamp`, ...), not as entries in the global registry
* Added custom formatters as fields on the `ConsoleWriter` struct, not as entries in the registry
* Removed `SetFormatter()` and `Formatter()` methods, and look up proper formatter
  (custom one or default) during formatting (`writeFields()`, `writeParts()`)
* Adjusted the test examples
* Minor source code cleanups

Related: rs#92
@karmi
Copy link
Contributor Author

karmi commented Oct 30, 2018

Travis failed on Go 1.9 with:

--- FAIL: TestNewWriter (0.00s)
	diode_test.go:29: Diode New Writer Test failed. got:, want:{"level":"debug","message":"test"}

I've tried to reproduce locally with Go 1.9.2 and the test didn't fail — I'll amend the commit and force push to trigger another build.

* Removed the global registry (map) of default formatters
* Therefore, removed the `init()` function
* Therefore, removed the `Reset()` method
* Added default formatters as package level variables (`consoleDefaultFormatTimestamp`, ...), not as entries in the global registry
* Added custom formatters as fields on the `ConsoleWriter` struct, not as entries in the registry
* Removed `SetFormatter()` and `Formatter()` methods, and look up proper formatter
  (custom one or default) during formatting (`writeFields()`, `writeParts()`)
* Adjusted the test examples
* Minor source code cleanups

Related: rs#92
@karmi
Copy link
Contributor Author

karmi commented Oct 30, 2018

I've took another stab at refactoring the functionality, and would like to get some feedback on the latest version of console.go.

The most important part is removing the global registry (map) of formatters, and therefore removing the need for any Reset() method, the init() function and other weird semantics.

The default formatters are added as package level variables (consoleDefaultFormatTimestamp, ...) and custom Formatters are added as regular fields of the ConsoleWriter struct. (Example of setting the fields directly here, example of using the constructor function option here.)

I've plugged this version into a real project where I'm using zerolog, and it behaves well. It still doesn't provide a way how to customize formatting a specific field (component) in my case, but that's not something which has to be solved right away...

I can imagine there's yet another approach possible, eg. adding a ConsoleFormatter struct, and attaching methods to it, and opening up the logic even more, but again, not sure if it has to be solved at once.

README.md Outdated
To customize the configuration and formatting:

```go
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}.Reset()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You forgot to update the doc to remove the .Reset() here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argh, sorry about that, fixed in fc34c71!

@rs
Copy link
Owner

rs commented Oct 30, 2018

Looks good. Please fix the doc and I'll merge.

@karmi
Copy link
Contributor Author

karmi commented Nov 5, 2018

Thanks! Do you want me to rebase & squash the commits or do you prefer to do it on your side?

@rs rs merged commit 96f91bb into rs:master Nov 5, 2018
@wvh
Copy link

wvh commented Nov 7, 2018

Hello,

The new ConsoleWriter creates consoleDefaultPartsOrder on init instead of when creating an instance of itself, so if someone changes the default fields – such as MessageFieldNameConsoleWriter will try to print the wrong field names.

For instance, if I set MessageFieldName to "msg", the console writer will try to print the original "message" field:

var out io.Writer = os.Stdout
zerolog.MessageFieldName = "msg"
out = zerolog.ConsoleWriter{Out: out}
logger = zerolog.New(out).With().Timestamp().Logger()
logger.Info().Msg("this message won't show")

results in (more or less):

15:26:27.818175 INF %!s(<nil>) at=main.go:171 component=main

... note the nil message field.

@karmi
Copy link
Contributor Author

karmi commented Nov 7, 2018

@wvh, argh, that's bad... I'll look into it!

@karmi
Copy link
Contributor Author

karmi commented Nov 7, 2018

I did the simplest thing which came to my mind, and made consoleDefaultPartsOrder a function, instead of a static slice: master...karmi:fix_console_custom_parts

In this way, it's called from Write() and therefore picks up the custom name — though I don't like it from the performance point of view much...

What do you think, @rs?

@rs
Copy link
Owner

rs commented Nov 7, 2018

No big deal IMHO. The console writer is not meant for production, but for dev anyway.

@karmi
Copy link
Contributor Author

karmi commented Nov 7, 2018

@rs, right, that's what I was thinking as well, just wasn't sure. I'll send the patch as a PR.

karmi added a commit to karmi/zerolog that referenced this pull request Nov 7, 2018
In order to prevent incorrect output when somebody uses a different name eg. for the "MessageFieldName",
the `consoleDefaultPartsOrder` variable has been switched to a function, which is called in `Write()`,
in order to pick up the custom name.

Related: rs#92
rs pushed a commit that referenced this pull request Nov 7, 2018
In order to prevent incorrect output when somebody uses a different name eg. for the "MessageFieldName",
the `consoleDefaultPartsOrder` variable has been switched to a function, which is called in `Write()`,
in order to pick up the custom name.

Related: #92
@wvh
Copy link

wvh commented Nov 8, 2018

I guess you could also force people to use the NewConsoleWriter constructor and set it there, but you'd have to make sure all struct fields can be set from the constructor or have setters exposed if you'd make the struct private.

By the way – thanks, I like the new look.

@karmi
Copy link
Contributor Author

karmi commented Nov 8, 2018

@wvh , right, that would solve the problem of initialization, but would break a lot of existing code, and that's what I wanted to prevent at all cost with the refactor...

@IngmarStein
Copy link
Contributor

Previously, ConsoleWriter could be used concurrently. Now, these package-level configuration options have introduced a data race in Write (see below). Is it intended that callers need to add synchronisation around ConsoleWriter?

==================
WARNING: DATA RACE
Read at 0x00000310ea0d by goroutine 27:
  …/vendor/github.com/rs/zerolog.ConsoleWriter.Write()
      /go/src/…/vendor/github.com/rs/zerolog/console.go:111 +0x656
  …/vendor/github.com/rs/zerolog.(*ConsoleWriter).Write()
      <autogenerated>:1 +0xf0
  …/vendor/github.com/rs/zerolog.levelWriterAdapter.WriteLevel()
      /go/src/…/vendor/github.com/rs/zerolog/writer.go:20 +0x66
  …/vendor/github.com/rs/zerolog.(*levelWriterAdapter).WriteLevel()
      <autogenerated>:1 +0xa4
  …/vendor/github.com/rs/zerolog.(*Event).write()
      /go/src/…/vendor/github.com/rs/zerolog/event.go:80 +0x2e0
  …/vendor/github.com/rs/zerolog.(*Event).msg()
      /go/src/…/vendor/github.com/rs/zerolog/event.go:139 +0x296
  …/vendor/github.com/rs/zerolog.(*Event).Msg()
      /go/src/…/vendor/github.com/rs/zerolog/event.go:110 +0x51
  …

Previous write at 0x00000310ea0d by goroutine 28:
  …/vendor/github.com/rs/zerolog.ConsoleWriter.Write()
      /go/src/…/vendor/github.com/rs/zerolog/console.go:112 +0x684
  …/vendor/github.com/rs/zerolog.(*ConsoleWriter).Write()
      <autogenerated>:1 +0xf0
  …/vendor/github.com/rs/zerolog.levelWriterAdapter.WriteLevel()
      /go/src/…/vendor/github.com/rs/zerolog/writer.go:20 +0x66
  …/vendor/github.com/rs/zerolog.(*levelWriterAdapter).WriteLevel()
      <autogenerated>:1 +0xa4
  …/vendor/github.com/rs/zerolog.(*Event).write()
      /go/src/…/vendor/github.com/rs/zerolog/event.go:80 +0x2e0
  …/vendor/github.com/rs/zerolog.(*Event).msg()
      /go/src/…/vendor/github.com/rs/zerolog/event.go:139 +0x296
  …/vendor/github.com/rs/zerolog.(*Event).Msg()
      /go/src/…/vendor/github.com/rs/zerolog/event.go:110 +0x51
  …

@karmi
Copy link
Contributor Author

karmi commented Dec 4, 2018

Hi @IngmarStein , I've tried locally, and was able to reproduce the race when eg. setting the timestamp option, coming from eg. this line:

consoleTimeFormat = consoleDefaultTimeFormat

I'll look into it, it definitely shouldn't require any synchronization from the calling code.

karmi added a commit to karmi/zerolog that referenced this pull request Dec 4, 2018
When the ConsoleWriter has been customized, the operations accessing
variables in the `Write()` method had race condition, as reported in
rs#92 (comment).

A simple mutex lock has been added around the section.

Example code:

```go
package main

import (
	"fmt"
	"os"
	"sync"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	var wg sync.WaitGroup

	log.Logger = log.With().Timestamp().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})

	// output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
	// log := zerolog.New(output).With().Timestamp().Logger()

	for i := 0; i < 10000; i++ {
		wg.Add(1)

		go func(i int) {
			defer wg.Done()
			log.Info().Str("foo", "bar").Msg(fmt.Sprintf("Hello from goroutine %d", i))
		}(i)
	}

	wg.Wait()
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants