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

Ability to build logger with custom Sink #572

Merged
merged 7 commits into from
Jun 27, 2018

Conversation

dimroc
Copy link
Contributor

@dimroc dimroc commented Apr 10, 2018

Introduce the ability to pass in Sinks enabling one to customize a logger's Writer (technically WriteSyncer) and Closer.

Sample usage:

func CreateLogger() *zap.Logger {
	config := zap.NewProductionConfig()
	file := path.Join("/tmp", "log.jsonl")

        RegisterSink("customsink",  func() (Sink, error) { return CustomSink{}, nil })
	config.OutputPaths = []string{"stdout", "customsink", file}
	zl, err := config.Build()

	if err != nil {
		log.Fatal(err)
	}
	return zl
}

// Complies with type Sink interface (zapcore.WriteSyncer, io.Closer)
type CustomSink struct{}

func (CustomSink) Sync() error  { return nil }
func (CustomSink) Close() error { return nil }

func (CustomSink) Write(p []byte) (int, error) {
	log.Print(string(p)) // send to syslog, or do anything
	return len(p), nil
}

@CLAassistant
Copy link

CLAassistant commented Apr 10, 2018

CLA assistant check
All committers have signed the CLA.

@codecov
Copy link

codecov bot commented Apr 11, 2018

Codecov Report

Merging #572 into master will decrease coverage by 0.23%.
The diff coverage is 95%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #572      +/-   ##
==========================================
- Coverage   97.47%   97.23%   -0.24%     
==========================================
  Files          39       40       +1     
  Lines        2017     2063      +46     
==========================================
+ Hits         1966     2006      +40     
- Misses         43       49       +6     
  Partials        8        8
Impacted Files Coverage Δ
writer.go 100% <100%> (ø) ⬆️
sink.go 92.59% <92.59%> (ø)
buffer/buffer.go 87.09% <0%> (-12.91%) ⬇️
zapcore/json_encoder.go 100% <0%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update eeedf31...3c29566. Read the comment docs.

@dimroc dimroc force-pushed the features/custom-sinks branch 3 times, most recently from d38dd8c to 1bc0592 Compare April 11, 2018 20:06
@dimroc dimroc changed the title Ability to build logger with custom SinkFactories Ability to build logger with custom Sink Apr 11, 2018
@dimroc
Copy link
Contributor Author

dimroc commented Apr 11, 2018

The core of the change can be found here https:/uber-go/zap/pull/572/files#diff-dc03dbc53655fac7a4c8d16b1b4144c1L59

Copy link
Contributor

@akshayjshah akshayjshah left a comment

Choose a reason for hiding this comment

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

Thank you for this PR, @dimroc! I'd like to discuss high-level direction with a few of the other maintainers before getting into the details of the code.

config.go Outdated
// passed options. For example, the option "stdout" will construct a logger
// that routes output to the stdout sink, but "/filepath" will fallback
// to writing to file.
func (cfg Config) BuildWithSinks(sm map[string]Sink, opts ...Option) (*Logger, error) {
Copy link
Contributor

@akshayjshah akshayjshah Apr 13, 2018

Choose a reason for hiding this comment

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

This is a nice idea - I'd like to make this portion of zap more extensible. I also like that this approach avoids adding global state.

However, this works very differently from extending encoders: for that, we use a global registry and a top-level RegisterEncoder function. For consistency, I'd actually prefer to add another global registry for sinks. (Generally, I much prefer dependency injection, but I think consistency is more important here.)

@prashantv and @abhinav, what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I'd prefer consistency as well here, and have a global registry for both encoders and sinks.

If we want users to be able to specify the exact registries, we'll likely want to add a single method that accepts both registries rather than a method for each.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback @akshayjshah @prashantv. I never saw RegisterEncoder but after seeing it, your requests make complete sense. I have since added a commit with RegisterSink to mimc that functionality. If @abhinav feels strongly about reverting back to the dependency injection style, no worries, we can just revert one commit.

writer.go Outdated
// Don't close standard out.
if sink, err := NewSink(path); err == nil {
writers = append(writers, sink)
closers = append(closers, sink)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept the two lists, writers and closers, because I didn't want to be too aggressive with the refactor and change the return value of
func open(paths []string) ([]zapcore.WriteSyncer, func(), error)
to
func open(paths []string) ([]Sink, func(), error)

It would have rippled out to affect a few other funcs and we would have needed an array of []zapcore.WriteSyncer for CombineWriteSyncers anyways.

writer.go Outdated
// Don't close standard out.
if sink, err := newSink(path); err == nil {
writers = append(writers, sink)
closers = append(closers, sink)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept the two lists, writers and closers, because I didn't want to be too aggressive with the refactor and change the return value of
func open(paths []string) ([]zapcore.WriteSyncer, func(), error)
to
func open(paths []string) ([]Sink, func(), error)

It would have rippled out to affect a few other funcs and we would have needed an array of []zapcore.WriteSyncer for CombineWriteSyncers anyways.

@etsangsplk
Copy link
Contributor

@akshayjshah
What's the decision??

@ghost
Copy link

ghost commented May 23, 2018

I'd also like to see this merged. Any update?

@se3000
Copy link

se3000 commented Jun 5, 2018

Would love to see this merged, it'd be very helpful for us. What's left to be done?

@akshayjshah
Copy link
Contributor

This is great. I'm going to push a commit tomorrow to un-export a few of the helper structs, but the core of this is excellent - thanks for the work and the refactoring, @dimroc.

To everyone waiting on this, thanks for your patience. Work and fatherhood distracted me for the past few months.

Expect a release with this functionality by the end of the week.

@dimroc did all the work for this feature already. This commit un-exports
NopCloserSink (since we're only using it in tests), adds an unexported function
to reset the global registry of sinks, and includes the user-supplied key in the
no-such-sink error message.
@akshayjshah akshayjshah merged commit 7e7e266 into uber-go:master Jun 27, 2018
@akshayjshah
Copy link
Contributor

Merged! I'm going to open PRs to prep a release tomorrow. We use this package extensively internally; for my own sanity and family life, I'm not going to cut a release on a Friday just in case something goes horribly wrong. I'll tag a new relase first thing monday morning.

}

func resetSinkRegistry() {
_sinkMutex.Lock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we have this mutex? It seems like we'd be masking a race where loggers might fail to build because the scheme isn't recognized by the sink?

Copy link
Contributor

Choose a reason for hiding this comment

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

Certainly possible. We lock around the encoder registry; IMO, consistency with that API is important here.


// RegisterSink adds a Sink at the given key so it can be referenced
// in config OutputPaths.
func RegisterSink(key string, sinkFactory func() (Sink, error)) error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will work well for static sinks, but I think we might need something more dynamic (e.g., a prefix match, or a URI scheme match). E.g., even for something like syslog, we'd probably need more info like port. I'd prefer to have less APIs exposed, so one that is more flexible might be better

Copy link
Contributor

@akshayjshah akshayjshah Jun 29, 2018

Choose a reason for hiding this comment

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

That's a good point - I hadn't thought about this use case.

I'd prefer to avoid a URI-based scheme here, since the current special-cased sinks don't use file://. Prefix match would work, as long as we ensure that we don't get any conflicting rules.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, URIs with special cases for stdout and stderr might be best 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

// in config OutputPaths.
func RegisterSink(key string, sinkFactory func() (Sink, error)) error {
_sinkMutex.Lock()
defer _sinkMutex.Unlock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

optional nit: blank after defer to separate it from the rest of the logic (same for newSink)

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 will do.

@DGollings
Copy link

I'm either the first or the least intelligent person to attempt to use this custom sink stuff. Took me quite some time and digging through issues/code to figure this out. To anyone wondering why the example doesn't work, it has changed slightly due to #606:

func CreateLogger() *zap.Logger {
	config := zap.NewProductionConfig()
	file := path.Join("/tmp", "log.jsonl")

        RegisterSink("customsink://",  func() (Sink, error) { return CustomSink{}, nil })
	config.OutputPaths = []string{"stdout", "customsink", file}
	zl, err := config.Build()

	if err != nil {
		log.Fatal(err)
	}
	return zl
}

// Complies with type Sink interface (zapcore.WriteSyncer, io.Closer)
type CustomSink struct{}

func (CustomSink) Sync() error  { return nil }
func (CustomSink) Close() error { return nil }

func (CustomSink) Write(p []byte) (int, error) {
	log.Print(string(p)) // send to syslog, or do anything
	return len(p), nil
}

RegisterSink("customsink", func() (Sink, error) { return CustomSink{}, nil })
should be
RegisterSink("customsink://", func() (Sink, error) { return CustomSink{}, nil })

cgxxv pushed a commit to cgxxv/zap that referenced this pull request Mar 25, 2022
Using the same global-registry pattern that we're using for encoders, let users register new sinks (like the built-in `stdout` and `stderr`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

7 participants