@@ -24,15 +24,19 @@ import (
24
24
"errors"
25
25
"fmt"
26
26
"io"
27
+ "net/url"
27
28
"os"
29
+ "strings"
28
30
"sync"
29
31
30
32
"go.uber.org/zap/zapcore"
31
33
)
32
34
35
+ const schemeFile = "file"
36
+
33
37
var (
34
38
_sinkMutex sync.RWMutex
35
- _sinkFactories map [string ]func () (Sink , error )
39
+ _sinkFactories map [string ]func (* url. URL ) (Sink , error ) // keyed by scheme
36
40
)
37
41
38
42
func init () {
@@ -42,18 +46,10 @@ func init() {
42
46
func resetSinkRegistry () {
43
47
_sinkMutex .Lock ()
44
48
defer _sinkMutex .Unlock ()
45
- _sinkFactories = map [string ]func () (Sink , error ){
46
- "stdout" : func () (Sink , error ) { return nopCloserSink {os .Stdout }, nil },
47
- "stderr" : func () (Sink , error ) { return nopCloserSink {os .Stderr }, nil },
48
- }
49
- }
50
49
51
- type errSinkNotFound struct {
52
- key string
53
- }
54
-
55
- func (e * errSinkNotFound ) Error () string {
56
- return fmt .Sprintf ("no sink found for %q" , e .key )
50
+ _sinkFactories = map [string ]func (* url.URL ) (Sink , error ){
51
+ schemeFile : newFileSink ,
52
+ }
57
53
}
58
54
59
55
// Sink defines the interface to write to and close logger destinations.
@@ -62,33 +58,104 @@ type Sink interface {
62
58
io.Closer
63
59
}
64
60
65
- // RegisterSink adds a Sink at the given key so it can be referenced
66
- // in config OutputPaths.
67
- func RegisterSink (key string , sinkFactory func () (Sink , error )) error {
61
+ type nopCloserSink struct { zapcore.WriteSyncer }
62
+
63
+ func (nopCloserSink ) Close () error { return nil }
64
+
65
+ type errSinkNotFound struct {
66
+ scheme string
67
+ }
68
+
69
+ func (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.
80
+ func RegisterSink (scheme string , factory func (* url.URL ) (Sink , error )) error {
68
81
_sinkMutex .Lock ()
69
82
defer _sinkMutex .Unlock ()
70
- if key == "" {
71
- return errors .New ("sink key cannot be blank" )
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 )
72
90
}
73
- if _ , ok := _sinkFactories [key ]; ok {
74
- return fmt .Errorf ("sink already registered for key %q" , key )
91
+ if _ , ok := _sinkFactories [normalized ]; ok {
92
+ return fmt .Errorf ("sink factory already registered for scheme %q" , normalized )
75
93
}
76
- _sinkFactories [key ] = sinkFactory
94
+ _sinkFactories [normalized ] = factory
77
95
return nil
78
96
}
79
97
80
- // newSink invokes the registered sink factory to create and return the
81
- // sink for the given key. Returns errSinkNotFound if the key cannot be found.
82
- func newSink (key string ) (Sink , error ) {
98
+ func 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
+
83
107
_sinkMutex .RLock ()
84
- defer _sinkMutex . RUnlock ()
85
- sinkFactory , ok := _sinkFactories [ key ]
108
+ factory , ok := _sinkFactories [ u . Scheme ]
109
+ _sinkMutex . RUnlock ()
86
110
if ! ok {
87
- return nil , & errSinkNotFound {key }
111
+ return nil , & errSinkNotFound {u . Scheme }
88
112
}
89
- return sinkFactory ( )
113
+ return factory ( u )
90
114
}
91
115
92
- type nopCloserSink struct { zapcore.WriteSyncer }
116
+ func 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 , 0644 )
140
+ }
93
141
94
- func (nopCloserSink ) Close () error { return nil }
142
+ func 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
+ }
0 commit comments