Skip to content

Commit 7cabba7

Browse files
authored
Open absolute paths as files, limited Windows support (#1159)
Ref #994, #1000 Alternate approach to #999, this approach brings absolute path support to Windows. It does so by checking for absolute paths and handling them using the file factory directly. To test different paths without trying to actually open them (UNC paths hit the network), this change also prefactors the sink registry into a separate type that allows stubbing of the `os.OpenFile` call. Note: This change does not bring full Windows support -- many tests still fail, and Windows file URIs don't work.
1 parent 7681a0a commit 7cabba7

File tree

5 files changed

+150
-51
lines changed

5 files changed

+150
-51
lines changed

sink.go

+59-41
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2016 Uber Technologies, Inc.
1+
// Copyright (c) 2016-2022 Uber Technologies, Inc.
22
//
33
// Permission is hereby granted, free of charge, to any person obtaining a copy
44
// of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +26,7 @@ import (
2626
"io"
2727
"net/url"
2828
"os"
29+
"path/filepath"
2930
"strings"
3031
"sync"
3132

@@ -34,34 +35,14 @@ import (
3435

3536
const schemeFile = "file"
3637

37-
var (
38-
_sinkMutex sync.RWMutex
39-
_sinkFactories map[string]func(*url.URL) (Sink, error) // keyed by scheme
40-
)
41-
42-
func init() {
43-
resetSinkRegistry()
44-
}
45-
46-
func resetSinkRegistry() {
47-
_sinkMutex.Lock()
48-
defer _sinkMutex.Unlock()
49-
50-
_sinkFactories = map[string]func(*url.URL) (Sink, error){
51-
schemeFile: newFileSink,
52-
}
53-
}
38+
var _sinkRegistry = newSinkRegistry()
5439

5540
// Sink defines the interface to write to and close logger destinations.
5641
type Sink interface {
5742
zapcore.WriteSyncer
5843
io.Closer
5944
}
6045

61-
type nopCloserSink struct{ zapcore.WriteSyncer }
62-
63-
func (nopCloserSink) Close() error { return nil }
64-
6546
type errSinkNotFound struct {
6647
scheme string
6748
}
@@ -70,16 +51,29 @@ func (e *errSinkNotFound) Error() string {
7051
return fmt.Sprintf("no sink found for scheme %q", e.scheme)
7152
}
7253

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 {
81-
_sinkMutex.Lock()
82-
defer _sinkMutex.Unlock()
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+
sr.RegisterSink(schemeFile, sr.newFileSinkFromURL)
70+
return sr
71+
}
72+
73+
// RegisterScheme registers the given factory for the specific scheme.
74+
func (sr *sinkRegistry) RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error {
75+
sr.mu.Lock()
76+
defer sr.mu.Unlock()
8377

8478
if scheme == "" {
8579
return errors.New("can't register a sink factory for empty string")
@@ -88,14 +82,22 @@ func RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error {
8882
if err != nil {
8983
return fmt.Errorf("%q is not a valid scheme: %v", scheme, err)
9084
}
91-
if _, ok := _sinkFactories[normalized]; ok {
85+
if _, ok := sr.factories[normalized]; ok {
9286
return fmt.Errorf("sink factory already registered for scheme %q", normalized)
9387
}
94-
_sinkFactories[normalized] = factory
88+
sr.factories[normalized] = factory
9589
return nil
9690
}
9791

98-
func newSink(rawURL string) (Sink, error) {
92+
func (sr *sinkRegistry) newSink(rawURL string) (Sink, error) {
93+
// URL parsing doesn't work well for Windows paths such as `c:\log.txt`, as scheme is set to
94+
// the drive, and path is unset unless `c:/log.txt` is used.
95+
// To avoid Windows-specific URL handling, we instead check IsAbs to open as a file.
96+
// filepath.IsAbs is OS-specific, so IsAbs('c:/log.txt') is false outside of Windows.
97+
if filepath.IsAbs(rawURL) {
98+
return sr.newFileSinkFromPath(rawURL)
99+
}
100+
99101
u, err := url.Parse(rawURL)
100102
if err != nil {
101103
return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err)
@@ -104,16 +106,27 @@ func newSink(rawURL string) (Sink, error) {
104106
u.Scheme = schemeFile
105107
}
106108

107-
_sinkMutex.RLock()
108-
factory, ok := _sinkFactories[u.Scheme]
109-
_sinkMutex.RUnlock()
109+
sr.mu.Lock()
110+
factory, ok := sr.factories[u.Scheme]
111+
sr.mu.Unlock()
110112
if !ok {
111113
return nil, &errSinkNotFound{u.Scheme}
112114
}
113115
return factory(u)
114116
}
115117

116-
func newFileSink(u *url.URL) (Sink, error) {
118+
// RegisterSink registers a user-supplied factory for all sinks with a
119+
// particular scheme.
120+
//
121+
// All schemes must be ASCII, valid under section 0.1 of RFC 3986
122+
// (https://tools.ietf.org/html/rfc3983#section-3.1), and must not already
123+
// have a factory registered. Zap automatically registers a factory for the
124+
// "file" scheme.
125+
func RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error {
126+
return _sinkRegistry.RegisterSink(scheme, factory)
127+
}
128+
129+
func (sr *sinkRegistry) newFileSinkFromURL(u *url.URL) (Sink, error) {
117130
if u.User != nil {
118131
return nil, fmt.Errorf("user and password not allowed with file URLs: got %v", u)
119132
}
@@ -130,13 +143,18 @@ func newFileSink(u *url.URL) (Sink, error) {
130143
if hn := u.Hostname(); hn != "" && hn != "localhost" {
131144
return nil, fmt.Errorf("file URLs must leave host empty or use localhost: got %v", u)
132145
}
133-
switch u.Path {
146+
147+
return sr.newFileSinkFromPath(u.Path)
148+
}
149+
150+
func (sr *sinkRegistry) newFileSinkFromPath(path string) (Sink, error) {
151+
switch path {
134152
case "stdout":
135153
return nopCloserSink{os.Stdout}, nil
136154
case "stderr":
137155
return nopCloserSink{os.Stderr}, nil
138156
}
139-
return os.OpenFile(u.Path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
157+
return sr.openFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
140158
}
141159

142160
func normalizeScheme(s string) (string, error) {

sink_test.go

+18-8
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,22 @@ import (
3333
"go.uber.org/zap/zapcore"
3434
)
3535

36+
func stubSinkRegistry(t testing.TB) *sinkRegistry {
37+
origSinkRegistry := _sinkRegistry
38+
t.Cleanup(func() {
39+
_sinkRegistry = origSinkRegistry
40+
})
41+
42+
r := newSinkRegistry()
43+
_sinkRegistry = r
44+
return r
45+
}
46+
3647
func TestRegisterSink(t *testing.T) {
48+
stubSinkRegistry(t)
49+
3750
const (
38-
memScheme = "m"
51+
memScheme = "mem"
3952
nopScheme = "no-op.1234"
4053
)
4154
var memCalls, nopCalls int
@@ -52,16 +65,14 @@ func TestRegisterSink(t *testing.T) {
5265
return nopCloserSink{zapcore.AddSync(io.Discard)}, nil
5366
}
5467

55-
defer resetSinkRegistry()
56-
5768
require.NoError(t, RegisterSink(strings.ToUpper(memScheme), memFactory), "Failed to register scheme %q.", memScheme)
58-
require.NoError(t, RegisterSink(nopScheme, nopFactory), "Failed to register scheme %q.", memScheme)
69+
require.NoError(t, RegisterSink(nopScheme, nopFactory), "Failed to register scheme %q.", nopScheme)
5970

6071
sink, close, err := Open(
6172
memScheme+"://somewhere",
6273
nopScheme+"://somewhere-else",
6374
)
64-
assert.NoError(t, err, "Unexpected error opening URLs with registered schemes.")
75+
require.NoError(t, err, "Unexpected error opening URLs with registered schemes.")
6576

6677
defer close()
6778

@@ -89,9 +100,8 @@ func TestRegisterSinkErrors(t *testing.T) {
89100

90101
for _, tt := range tests {
91102
t.Run("scheme-"+tt.scheme, func(t *testing.T) {
92-
defer resetSinkRegistry()
93-
94-
err := RegisterSink(tt.scheme, nopFactory)
103+
r := newSinkRegistry()
104+
err := r.RegisterSink(tt.scheme, nopFactory)
95105
assert.ErrorContains(t, err, tt.err)
96106
})
97107
}

sink_windows_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) 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+
//go:build windows
22+
23+
package zap
24+
25+
import (
26+
"os"
27+
"testing"
28+
29+
"github.com/stretchr/testify/assert"
30+
)
31+
32+
func TestWindowsPaths(t *testing.T) {
33+
// See https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
34+
tests := []struct {
35+
msg string
36+
path string
37+
}{
38+
{
39+
msg: "local path with drive",
40+
path: `c:\log.json`,
41+
},
42+
{
43+
msg: "local path with drive using forward slash",
44+
path: `c:/log.json`,
45+
},
46+
{
47+
msg: "local path without drive",
48+
path: `\Temp\log.json`,
49+
},
50+
{
51+
msg: "unc path",
52+
path: `\\Server2\Logs\log.json`,
53+
},
54+
}
55+
56+
for _, tt := range tests {
57+
t.Run(tt.msg, func(t *testing.T) {
58+
sr := newSinkRegistry()
59+
60+
openFilename := "<not called>"
61+
sr.openFile = func(filename string, _ int, _ os.FileMode) (*os.File, error) {
62+
openFilename = filename
63+
return nil, assert.AnError
64+
}
65+
66+
_, err := sr.newSink(tt.path)
67+
assert.Equal(t, assert.AnError, err, "expect stub error from OpenFile")
68+
assert.Equal(t, tt.path, openFilename, "unexpected path opened")
69+
})
70+
}
71+
}

writer.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func open(paths []string) ([]zapcore.WriteSyncer, func(), error) {
6868

6969
var openErr error
7070
for _, path := range paths {
71-
sink, err := newSink(path)
71+
sink, err := _sinkRegistry.newSink(path)
7272
if err != nil {
7373
openErr = multierr.Append(openErr, fmt.Errorf("open sink %q: %w", path, err))
7474
continue

writer_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ func (w *testWriter) Sync() error {
240240
}
241241

242242
func TestOpenWithErroringSinkFactory(t *testing.T) {
243-
defer resetSinkRegistry()
243+
stubSinkRegistry(t)
244244

245245
msg := "expected factory error"
246246
factory := func(_ *url.URL) (Sink, error) {

0 commit comments

Comments
 (0)