-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
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.