github.com/Laisky/zap@v1.27.0/sink.go (about) 1 // Copyright (c) 2016-2022 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 21 package zap 22 23 import ( 24 "errors" 25 "fmt" 26 "io" 27 "net/url" 28 "os" 29 "path/filepath" 30 "strings" 31 "sync" 32 33 "github.com/Laisky/zap/zapcore" 34 ) 35 36 const schemeFile = "file" 37 38 var _sinkRegistry = newSinkRegistry() 39 40 // Sink defines the interface to write to and close logger destinations. 41 type Sink interface { 42 zapcore.WriteSyncer 43 io.Closer 44 } 45 46 type errSinkNotFound struct { 47 scheme string 48 } 49 50 func (e *errSinkNotFound) Error() string { 51 return fmt.Sprintf("no sink found for scheme %q", e.scheme) 52 } 53 54 type nopCloserSink struct{ zapcore.WriteSyncer } 55 56 func (nopCloserSink) Close() error { return nil } 57 58 type sinkRegistry struct { 59 mu sync.Mutex 60 factories map[string]func(*url.URL) (Sink, error) // keyed by scheme 61 openFile func(string, int, os.FileMode) (*os.File, error) // type matches os.OpenFile 62 } 63 64 func newSinkRegistry() *sinkRegistry { 65 sr := &sinkRegistry{ 66 factories: make(map[string]func(*url.URL) (Sink, error)), 67 openFile: os.OpenFile, 68 } 69 // Infallible operation: the registry is empty, so we can't have a conflict. 70 _ = sr.RegisterSink(schemeFile, sr.newFileSinkFromURL) 71 return sr 72 } 73 74 // RegisterScheme registers the given factory for the specific scheme. 75 func (sr *sinkRegistry) RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error { 76 sr.mu.Lock() 77 defer sr.mu.Unlock() 78 79 if scheme == "" { 80 return errors.New("can't register a sink factory for empty string") 81 } 82 normalized, err := normalizeScheme(scheme) 83 if err != nil { 84 return fmt.Errorf("%q is not a valid scheme: %v", scheme, err) 85 } 86 if _, ok := sr.factories[normalized]; ok { 87 return fmt.Errorf("sink factory already registered for scheme %q", normalized) 88 } 89 sr.factories[normalized] = factory 90 return nil 91 } 92 93 func (sr *sinkRegistry) newSink(rawURL string) (Sink, error) { 94 // URL parsing doesn't work well for Windows paths such as `c:\log.txt`, as scheme is set to 95 // the drive, and path is unset unless `c:/log.txt` is used. 96 // To avoid Windows-specific URL handling, we instead check IsAbs to open as a file. 97 // filepath.IsAbs is OS-specific, so IsAbs('c:/log.txt') is false outside of Windows. 98 if filepath.IsAbs(rawURL) { 99 return sr.newFileSinkFromPath(rawURL) 100 } 101 102 u, err := url.Parse(rawURL) 103 if err != nil { 104 return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err) 105 } 106 if u.Scheme == "" { 107 u.Scheme = schemeFile 108 } 109 110 sr.mu.Lock() 111 factory, ok := sr.factories[u.Scheme] 112 sr.mu.Unlock() 113 if !ok { 114 return nil, &errSinkNotFound{u.Scheme} 115 } 116 return factory(u) 117 } 118 119 // RegisterSink registers a user-supplied factory for all sinks with a 120 // particular scheme. 121 // 122 // All schemes must be ASCII, valid under section 0.1 of RFC 3986 123 // (https://tools.ietf.org/html/rfc3983#section-3.1), and must not already 124 // have a factory registered. Zap automatically registers a factory for the 125 // "file" scheme. 126 func RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error { 127 return _sinkRegistry.RegisterSink(scheme, factory) 128 } 129 130 func (sr *sinkRegistry) newFileSinkFromURL(u *url.URL) (Sink, error) { 131 if u.User != nil { 132 return nil, fmt.Errorf("user and password not allowed with file URLs: got %v", u) 133 } 134 if u.Fragment != "" { 135 return nil, fmt.Errorf("fragments not allowed with file URLs: got %v", u) 136 } 137 if u.RawQuery != "" { 138 return nil, fmt.Errorf("query parameters not allowed with file URLs: got %v", u) 139 } 140 // Error messages are better if we check hostname and port separately. 141 if u.Port() != "" { 142 return nil, fmt.Errorf("ports not allowed with file URLs: got %v", u) 143 } 144 if hn := u.Hostname(); hn != "" && hn != "localhost" { 145 return nil, fmt.Errorf("file URLs must leave host empty or use localhost: got %v", u) 146 } 147 148 return sr.newFileSinkFromPath(u.Path) 149 } 150 151 func (sr *sinkRegistry) newFileSinkFromPath(path string) (Sink, error) { 152 switch path { 153 case "stdout": 154 return nopCloserSink{os.Stdout}, nil 155 case "stderr": 156 return nopCloserSink{os.Stderr}, nil 157 } 158 return sr.openFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666) 159 } 160 161 func normalizeScheme(s string) (string, error) { 162 // https://tools.ietf.org/html/rfc3986#section-3.1 163 s = strings.ToLower(s) 164 if first := s[0]; 'a' > first || 'z' < first { 165 return "", errors.New("must start with a letter") 166 } 167 for i := 1; i < len(s); i++ { // iterate over bytes, not runes 168 c := s[i] 169 switch { 170 case 'a' <= c && c <= 'z': 171 continue 172 case '0' <= c && c <= '9': 173 continue 174 case c == '.' || c == '+' || c == '-': 175 continue 176 } 177 return "", fmt.Errorf("may not contain %q", c) 178 } 179 return s, nil 180 }