-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Make user-supplied sinks operate on URIs #606
Changes from 3 commits
f8b6d35
f5eee05
0db234b
6e33ddf
8dccd5c
c7f4f5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,15 +24,19 @@ import ( | |
"errors" | ||
"fmt" | ||
"io" | ||
"net/url" | ||
"os" | ||
"strings" | ||
"sync" | ||
|
||
"go.uber.org/zap/zapcore" | ||
) | ||
|
||
const schemeFile = "file" | ||
|
||
var ( | ||
_sinkMutex sync.RWMutex | ||
_sinkFactories map[string]func() (Sink, error) | ||
_sinkFactories map[string]func(*url.URL) (Sink, error) // keyed by scheme | ||
) | ||
|
||
func init() { | ||
|
@@ -42,18 +46,10 @@ func init() { | |
func resetSinkRegistry() { | ||
_sinkMutex.Lock() | ||
defer _sinkMutex.Unlock() | ||
_sinkFactories = map[string]func() (Sink, error){ | ||
"stdout": func() (Sink, error) { return nopCloserSink{os.Stdout}, nil }, | ||
"stderr": func() (Sink, error) { return nopCloserSink{os.Stderr}, nil }, | ||
} | ||
} | ||
|
||
type errSinkNotFound struct { | ||
key string | ||
} | ||
|
||
func (e *errSinkNotFound) Error() string { | ||
return fmt.Sprintf("no sink found for %q", e.key) | ||
_sinkFactories = map[string]func(*url.URL) (Sink, error){ | ||
schemeFile: newFileSink, | ||
} | ||
} | ||
|
||
// Sink defines the interface to write to and close logger destinations. | ||
|
@@ -62,33 +58,92 @@ type Sink interface { | |
io.Closer | ||
} | ||
|
||
// 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 { | ||
type nopCloserSink struct{ zapcore.WriteSyncer } | ||
|
||
func (nopCloserSink) Close() error { return nil } | ||
|
||
type errSinkNotFound struct { | ||
scheme string | ||
} | ||
|
||
func (e *errSinkNotFound) Error() string { | ||
return fmt.Sprintf("no sink found for scheme %q", e.scheme) | ||
} | ||
|
||
// RegisterSink registers a user-supplied factory for all sinks with a | ||
// particular scheme. | ||
// | ||
// All schemes must be ASCII, valid under section 3.1 of RFC 3986 | ||
// (https://tools.ietf.org/html/rfc3986#section-3.1), and may not already have | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "must not" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, fixed. |
||
// a factory registered. Zap automatically registers a factory for the "file" | ||
// scheme. | ||
func RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this a breaking change? The factory function didn't accept a callback before. |
||
_sinkMutex.Lock() | ||
defer _sinkMutex.Unlock() | ||
if key == "" { | ||
return errors.New("sink key cannot be blank") | ||
|
||
if scheme == "" { | ||
return errors.New("can't register a sink factory for empty string") | ||
} | ||
normalized, err := normalizeScheme(scheme) | ||
if err != nil { | ||
return fmt.Errorf("%q is not a valid scheme: %v", scheme, err) | ||
} | ||
if _, ok := _sinkFactories[key]; ok { | ||
return fmt.Errorf("sink already registered for key %q", key) | ||
if _, ok := _sinkFactories[normalized]; ok { | ||
return fmt.Errorf("sink factory already registered for scheme %q", normalized) | ||
} | ||
_sinkFactories[key] = sinkFactory | ||
_sinkFactories[normalized] = factory | ||
return nil | ||
} | ||
|
||
// newSink invokes the registered sink factory to create and return the | ||
// sink for the given key. Returns errSinkNotFound if the key cannot be found. | ||
func newSink(key string) (Sink, error) { | ||
func newSink(rawURL string) (Sink, error) { | ||
u, err := url.Parse(rawURL) | ||
if err != nil { | ||
return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err) | ||
} | ||
if u.Scheme == "" { | ||
u.Scheme = schemeFile | ||
} | ||
|
||
_sinkMutex.RLock() | ||
defer _sinkMutex.RUnlock() | ||
sinkFactory, ok := _sinkFactories[key] | ||
|
||
factory, ok := _sinkFactories[u.Scheme] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optionally: can we hold the _sinkMutex.RLock()
factory, ok := _sinkFactories[u.Scheme]
_sinkMutex.RUnlock() Shouldn't be an issue since it's just an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, can do. |
||
if !ok { | ||
return nil, &errSinkNotFound{key} | ||
return nil, &errSinkNotFound{u.Scheme} | ||
} | ||
return sinkFactory() | ||
return factory(u) | ||
} | ||
|
||
type nopCloserSink struct{ zapcore.WriteSyncer } | ||
func newFileSink(u *url.URL) (Sink, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional: should we ensure there's no fragments etc? ignoring seems OK too, but it might be a little surprising There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. |
||
switch u.Path { | ||
case "stdout": | ||
return nopCloserSink{os.Stdout}, nil | ||
case "stderr": | ||
return nopCloserSink{os.Stderr}, nil | ||
} | ||
return os.OpenFile(u.Path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) | ||
} | ||
|
||
func (nopCloserSink) Close() error { return nil } | ||
func normalizeScheme(s string) (string, error) { | ||
// https://tools.ietf.org/html/rfc3986#section-3.1 | ||
s = strings.ToLower(s) | ||
if first := s[0]; 'a' > first || 'z' < first { | ||
return "", errors.New("must start with a letter") | ||
} | ||
if len(s) < 2 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the rest of the code will work fine for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, just left over from previous iterations on the code. My bad. |
||
return s, nil | ||
} | ||
for i := 1; i < len(s); i++ { // iterate over bytes, not runes | ||
c := s[i] | ||
switch { | ||
case 'a' <= c && c <= 'z': | ||
continue | ||
case '0' <= c && c <= '9': | ||
continue | ||
case c == '.' || c == '+' || c == '-': | ||
continue | ||
} | ||
return "", fmt.Errorf("may not contain %q", string(c)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely. |
||
} | ||
return s, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we mention that filenames are also supported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, can do.