1// Copyright (c) 2016 Uber Technologies, Inc. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a copy 4// of this software and associated documentation files (the "Software"), to deal 5// in the Software without restriction, including without limitation the rights 6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7// copies of the Software, and to permit persons to whom the Software is 8// furnished to do so, subject to the following conditions: 9// 10// The above copyright notice and this permission notice shall be included in 11// all copies or substantial portions of the Software. 12// 13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19// THE SOFTWARE. 20 21package zap 22 23import ( 24 "errors" 25 "fmt" 26 "io" 27 "net/url" 28 "os" 29 "strings" 30 "sync" 31 32 "go.uber.org/zap/zapcore" 33) 34 35const schemeFile = "file" 36 37var ( 38 _sinkMutex sync.RWMutex 39 _sinkFactories map[string]func(*url.URL) (Sink, error) // keyed by scheme 40) 41 42func init() { 43 resetSinkRegistry() 44} 45 46func resetSinkRegistry() { 47 _sinkMutex.Lock() 48 defer _sinkMutex.Unlock() 49 50 _sinkFactories = map[string]func(*url.URL) (Sink, error){ 51 schemeFile: newFileSink, 52 } 53} 54 55// Sink defines the interface to write to and close logger destinations. 56type Sink interface { 57 zapcore.WriteSyncer 58 io.Closer 59} 60 61type nopCloserSink struct{ zapcore.WriteSyncer } 62 63func (nopCloserSink) Close() error { return nil } 64 65type errSinkNotFound struct { 66 scheme string 67} 68 69func (e *errSinkNotFound) Error() string { 70 return fmt.Sprintf("no sink found for scheme %q", e.scheme) 71} 72 73// RegisterSink registers a user-supplied factory for all sinks with a 74// particular scheme. 75// 76// All schemes must be ASCII, valid under section 3.1 of RFC 3986 77// (https://tools.ietf.org/html/rfc3986#section-3.1), and must not already 78// have a factory registered. Zap automatically registers a factory for the 79// "file" scheme. 80func RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error { 81 _sinkMutex.Lock() 82 defer _sinkMutex.Unlock() 83 84 if scheme == "" { 85 return errors.New("can't register a sink factory for empty string") 86 } 87 normalized, err := normalizeScheme(scheme) 88 if err != nil { 89 return fmt.Errorf("%q is not a valid scheme: %v", scheme, err) 90 } 91 if _, ok := _sinkFactories[normalized]; ok { 92 return fmt.Errorf("sink factory already registered for scheme %q", normalized) 93 } 94 _sinkFactories[normalized] = factory 95 return nil 96} 97 98func newSink(rawURL string) (Sink, error) { 99 u, err := url.Parse(rawURL) 100 if err != nil { 101 return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err) 102 } 103 if u.Scheme == "" { 104 u.Scheme = schemeFile 105 } 106 107 _sinkMutex.RLock() 108 factory, ok := _sinkFactories[u.Scheme] 109 _sinkMutex.RUnlock() 110 if !ok { 111 return nil, &errSinkNotFound{u.Scheme} 112 } 113 return factory(u) 114} 115 116func newFileSink(u *url.URL) (Sink, error) { 117 if u.User != nil { 118 return nil, fmt.Errorf("user and password not allowed with file URLs: got %v", u) 119 } 120 if u.Fragment != "" { 121 return nil, fmt.Errorf("fragments not allowed with file URLs: got %v", u) 122 } 123 if u.RawQuery != "" { 124 return nil, fmt.Errorf("query parameters not allowed with file URLs: got %v", u) 125 } 126 // Error messages are better if we check hostname and port separately. 127 if u.Port() != "" { 128 return nil, fmt.Errorf("ports not allowed with file URLs: got %v", u) 129 } 130 if hn := u.Hostname(); hn != "" && hn != "localhost" { 131 return nil, fmt.Errorf("file URLs must leave host empty or use localhost: got %v", u) 132 } 133 switch u.Path { 134 case "stdout": 135 return nopCloserSink{os.Stdout}, nil 136 case "stderr": 137 return nopCloserSink{os.Stderr}, nil 138 } 139 return os.OpenFile(u.Path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 140} 141 142func normalizeScheme(s string) (string, error) { 143 // https://tools.ietf.org/html/rfc3986#section-3.1 144 s = strings.ToLower(s) 145 if first := s[0]; 'a' > first || 'z' < first { 146 return "", errors.New("must start with a letter") 147 } 148 for i := 1; i < len(s); i++ { // iterate over bytes, not runes 149 c := s[i] 150 switch { 151 case 'a' <= c && c <= 'z': 152 continue 153 case '0' <= c && c <= '9': 154 continue 155 case c == '.' || c == '+' || c == '-': 156 continue 157 } 158 return "", fmt.Errorf("may not contain %q", c) 159 } 160 return s, nil 161} 162