Skip to content

Commit 212a3be

Browse files
authored
Add Close method to logp.Logger (#205)
Add a close method to logp.Logger responsible for closing any underlying core/output. All of our implementations of zapcore.Core that use an output that can be closed now implement io.Closer. This allows for closing any underlying files used by any output, which can prevent issues on Windows. It also removes a workaround for windows in a test because the log files could not be closed.
1 parent c77acef commit 212a3be

File tree

12 files changed

+329
-69
lines changed

12 files changed

+329
-69
lines changed

NOTICE.txt

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,6 +2055,72 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20552055
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
20562056

20572057

2058+
--------------------------------------------------------------------------------
2059+
Dependency : golang.org/x/text
2060+
Version: v0.14.0
2061+
Licence type (autodetected): BSD-3-Clause
2062+
--------------------------------------------------------------------------------
2063+
2064+
Contents of probable licence file $GOMODCACHE/golang.org/x/[email protected]/LICENSE:
2065+
2066+
Copyright (c) 2009 The Go Authors. All rights reserved.
2067+
2068+
Redistribution and use in source and binary forms, with or without
2069+
modification, are permitted provided that the following conditions are
2070+
met:
2071+
2072+
* Redistributions of source code must retain the above copyright
2073+
notice, this list of conditions and the following disclaimer.
2074+
* Redistributions in binary form must reproduce the above
2075+
copyright notice, this list of conditions and the following disclaimer
2076+
in the documentation and/or other materials provided with the
2077+
distribution.
2078+
* Neither the name of Google Inc. nor the names of its
2079+
contributors may be used to endorse or promote products derived from
2080+
this software without specific prior written permission.
2081+
2082+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
2083+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
2084+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
2085+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
2086+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
2087+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
2088+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2089+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2090+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2091+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2092+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2093+
2094+
2095+
--------------------------------------------------------------------------------
2096+
Dependency : gopkg.in/mcuadros/go-syslog.v2
2097+
Version: v2.3.0
2098+
Licence type (autodetected): MIT
2099+
--------------------------------------------------------------------------------
2100+
2101+
Contents of probable licence file $GOMODCACHE/gopkg.in/mcuadros/[email protected]/LICENSE:
2102+
2103+
Copyright (c) 2013 Máximo Cuadros
2104+
2105+
Permission is hereby granted, free of charge, to any person obtaining a copy
2106+
of this software and associated documentation files (the "Software"), to deal
2107+
in the Software without restriction, including without limitation the rights
2108+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2109+
copies of the Software, and to permit persons to whom the Software is furnished
2110+
to do so, subject to the following conditions:
2111+
2112+
The above copyright notice and this permission notice shall be included in all
2113+
copies or substantial portions of the Software.
2114+
2115+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2116+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2117+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2118+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2119+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2120+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121+
THE SOFTWARE.
2122+
2123+
20582124
--------------------------------------------------------------------------------
20592125
Dependency : gopkg.in/yaml.v2
20602126
Version: v2.4.0
@@ -5038,43 +5104,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
50385104
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
50395105

50405106

5041-
--------------------------------------------------------------------------------
5042-
Dependency : golang.org/x/text
5043-
Version: v0.14.0
5044-
Licence type (autodetected): BSD-3-Clause
5045-
--------------------------------------------------------------------------------
5046-
5047-
Contents of probable licence file $GOMODCACHE/golang.org/x/[email protected]/LICENSE:
5048-
5049-
Copyright (c) 2009 The Go Authors. All rights reserved.
5050-
5051-
Redistribution and use in source and binary forms, with or without
5052-
modification, are permitted provided that the following conditions are
5053-
met:
5054-
5055-
* Redistributions of source code must retain the above copyright
5056-
notice, this list of conditions and the following disclaimer.
5057-
* Redistributions in binary form must reproduce the above
5058-
copyright notice, this list of conditions and the following disclaimer
5059-
in the documentation and/or other materials provided with the
5060-
distribution.
5061-
* Neither the name of Google Inc. nor the names of its
5062-
contributors may be used to endorse or promote products derived from
5063-
this software without specific prior written permission.
5064-
5065-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5066-
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5067-
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
5068-
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
5069-
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
5070-
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
5071-
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
5072-
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
5073-
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
5074-
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
5075-
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
5076-
5077-
50785107
--------------------------------------------------------------------------------
50795108
Dependency : golang.org/x/tools
50805109
Version: v0.6.0

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ require (
2323
golang.org/x/crypto v0.22.0
2424
golang.org/x/net v0.23.0
2525
golang.org/x/sys v0.19.0
26+
golang.org/x/text v0.14.0
27+
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
2628
gopkg.in/yaml.v2 v2.4.0
2729
)
2830

@@ -51,7 +53,6 @@ require (
5153
go.uber.org/multierr v1.11.0 // indirect
5254
golang.org/x/mod v0.8.0 // indirect
5355
golang.org/x/sync v0.6.0 // indirect
54-
golang.org/x/text v0.14.0 // indirect
5556
golang.org/x/tools v0.6.0 // indirect
5657
gopkg.in/yaml.v3 v3.0.1 // indirect
5758
howett.net/plist v1.0.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
227227
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
228228
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
229229
gopkg.in/hjson/hjson-go.v3 v3.0.1/go.mod h1:X6zrTSVeImfwfZLfgQdInl9mWjqPqgH90jom9nym/lw=
230+
gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD79kk=
231+
gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
230232
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
231233
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
232234
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

logp/core.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ type coreLogger struct {
6363
observedLogs *observer.ObservedLogs // Contains events generated while in observation mode (a testing mode).
6464
}
6565

66+
type closerCore struct {
67+
zapcore.Core
68+
io.Closer
69+
}
70+
71+
func (c *closerCore) With(fields []zapcore.Field) zapcore.Core {
72+
c.Core = c.Core.With(fields)
73+
return c
74+
}
75+
6676
// Configure configures the logp package.
6777
func Configure(cfg Config) error {
6878
return ConfigureWithOutputs(cfg)
@@ -314,14 +324,35 @@ func makeFileOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, error)
314324
return nil, fmt.Errorf("failed to create file rotator: %w", err)
315325
}
316326

317-
return newCore(buildEncoder(cfg), rotator, enab), nil
327+
// Keep the same behaviour from before we introduced the closerCore.
328+
core, err := newCore(buildEncoder(cfg), rotator, enab), nil
329+
if err != nil {
330+
return core, err
331+
}
332+
333+
cc := closerCore{
334+
Core: core,
335+
Closer: rotator,
336+
}
337+
338+
return &cc, err
318339
}
319340

320341
func newCore(enc zapcore.Encoder, ws zapcore.WriteSyncer, enab zapcore.LevelEnabler) zapcore.Core {
321342
return wrappedCore(zapcore.NewCore(enc, ws, enab))
322343
}
323344
func wrappedCore(core zapcore.Core) zapcore.Core {
324-
return ecszap.WrapCore(core)
345+
wc := ecszap.WrapCore(core)
346+
347+
if closeCore, ok := core.(io.Closer); ok {
348+
cc := closerCore{
349+
Core: wc,
350+
Closer: closeCore,
351+
}
352+
return &cc
353+
}
354+
355+
return wc
325356
}
326357

327358
func globalLogger() *zap.Logger {
@@ -406,3 +437,17 @@ func (m multiCore) Sync() error {
406437
}
407438
return errors.Join(errs...)
408439
}
440+
441+
// Close calls Close on any core that implements io.Closer.
442+
// All returned errors are joined by errors.Join and returned.
443+
func (m multiCore) Close() error {
444+
errs := []error{}
445+
for _, core := range m.cores {
446+
if closer, ok := core.(io.Closer); ok {
447+
closeErr := closer.Close()
448+
errs = append(errs, closeErr)
449+
}
450+
}
451+
452+
return errors.Join(errs...)
453+
}

logp/core_linux_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//go:build linux
19+
20+
package logp
21+
22+
import (
23+
"io"
24+
"testing"
25+
26+
"go.uber.org/zap/zapcore"
27+
"gopkg.in/mcuadros/go-syslog.v2"
28+
)
29+
30+
// TestSyslogOutputCanBeClosed instantiates a syslog output and ensures it
31+
// implements `io.Close`.
32+
//
33+
// We call close to ensure it does not return an error.
34+
//
35+
// Our syslog is hardcoded to connect via Unix sockets. The container
36+
// we use to run the tests does not have a syslog listening on Unix socket,
37+
// so we instantiate our own.
38+
func TestSyslogOutputCanBeClosed(t *testing.T) {
39+
channel := make(syslog.LogPartsChannel)
40+
handler := syslog.NewChannelHandler(channel)
41+
42+
server := syslog.NewServer()
43+
server.SetFormat(syslog.RFC5424)
44+
server.SetHandler(handler)
45+
46+
if err := server.ListenUnixgram("/var/run/syslog"); err != nil {
47+
t.Errorf("cannot configure syslog to listen on '/var/run/syslog/': %s", err)
48+
t.Log("You might already have a syslog running, this test assumes " +
49+
"there is no syslog running on the host. You can run this test " +
50+
"in a Docker container (adjust the image tag to the current " +
51+
"Go version):\n" +
52+
"'docker run --rm -it -v $PWD:$PWD -w $PWD golang:1.21.10 go test ./logp'")
53+
}
54+
55+
if err := server.Boot(); err != nil {
56+
t.Fatalf("cannot start syslog server: %s", err)
57+
}
58+
59+
t.Cleanup(func() {
60+
// ignore the error, we just need it to accept
61+
// connections from our syslog output
62+
_ = server.Kill()
63+
})
64+
65+
cfg := DefaultConfig(DefaultEnvironment)
66+
cfg.ToFiles = true
67+
68+
syslogOutput, err := makeSyslogOutput(cfg, zapcore.DebugLevel)
69+
if err != nil {
70+
t.Fatalf("cannot create syslog output: %s", err)
71+
}
72+
73+
closer, ok := syslogOutput.(io.Closer)
74+
if !ok {
75+
t.Fatal("the 'Syslog Output' does not implement io.Closer")
76+
}
77+
if err := closer.Close(); err != nil {
78+
t.Fatalf("Close must not return any error, got: %s", err)
79+
}
80+
}

logp/core_test.go

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
golog "log"
2626
"os"
2727
"path/filepath"
28-
"runtime"
2928
"strings"
3029
"testing"
3130

@@ -222,33 +221,10 @@ func TestLoggingECSFields(t *testing.T) {
222221
}
223222

224223
func TestCreatingNewLoggerWithDifferentOutput(t *testing.T) {
225-
var tempDir1, tempDir2 string
226-
// Because of the way logp and zap work, when the test finishes, the log
227-
// file is still open, this creates a problem on Windows because the
228-
// temporary directory cannot be removed if a file inside it is still
229-
// open.
230-
// See https://github.com/elastic/elastic-agent-libs/issues/179
231-
// for more details
232-
//
233-
// To circumvent this problem on Windows we use os.MkdirTemp
234-
// leaving it behind and delegating to the OS the responsibility
235-
// of cleaning it up (usually on restart).
236-
if runtime.GOOS == "windows" {
237-
var err error
238-
tempDir1, err = os.MkdirTemp("", t.Name()+"-*")
239-
if err != nil {
240-
t.Fatalf("could not create temporary directory: %s", err)
241-
}
242-
tempDir2, err = os.MkdirTemp("", t.Name()+"-*")
243-
if err != nil {
244-
t.Fatalf("could not create temporary directory: %s", err)
245-
}
246-
} else {
247-
// We have no problems on Linux and Darwin, so we can rely on t.TempDir
248-
// that will remove the files once the tests finishes.
249-
tempDir1 = t.TempDir()
250-
tempDir2 = t.TempDir()
251-
}
224+
// We have no problems on Linux and Darwin, so we can rely on t.TempDir
225+
// that will remove the files once the tests finishes.
226+
tempDir1 := t.TempDir()
227+
tempDir2 := t.TempDir()
252228

253229
secondLoggerMessage := "this is a log message"
254230
secondLoggerName := t.Name() + "-second"
@@ -279,6 +255,11 @@ func TestCreatingNewLoggerWithDifferentOutput(t *testing.T) {
279255
if err := logger.Sync(); err != nil {
280256
t.Fatalf("could not sync log file from fist logger: %s", err)
281257
}
258+
t.Cleanup(func() {
259+
if err := logger.Close(); err != nil {
260+
t.Fatalf("could not close first logger: %s", err)
261+
}
262+
})
282263

283264
// Actually clones the logger and use the "WithFileOutput" function
284265
secondCfg := DefaultConfig(DefaultEnvironment)
@@ -303,6 +284,11 @@ func TestCreatingNewLoggerWithDifferentOutput(t *testing.T) {
303284
if err := secondLogger.Sync(); err != nil {
304285
t.Fatalf("could not sync log file from second logger: %s", err)
305286
}
287+
t.Cleanup(func() {
288+
if err := secondLogger.Close(); err != nil {
289+
t.Fatalf("could not close second logger: %s", err)
290+
}
291+
})
306292

307293
// Write again with the first logger to ensure it has not been affected
308294
// by the new configuration on the second logger.
@@ -631,6 +617,24 @@ func TestCreateLogOutputAllDisabled(t *testing.T) {
631617
}
632618
}
633619

620+
func TestCoresCanBeClosed(t *testing.T) {
621+
cfg := DefaultConfig(DefaultEnvironment)
622+
cfg.ToFiles = true
623+
624+
fileOutput, err := makeFileOutput(cfg, zapcore.DebugLevel)
625+
if err != nil {
626+
t.Fatalf("cannot create file output: %s", err)
627+
}
628+
629+
closer, ok := fileOutput.(io.Closer)
630+
if !ok {
631+
t.Fatal("the 'File Output' does not implement io.Closer")
632+
}
633+
if err := closer.Close(); err != nil {
634+
t.Fatalf("Close must not return any error, got: %s", err)
635+
}
636+
}
637+
634638
func strField(key, val string) zapcore.Field {
635639
return zapcore.Field{Type: zapcore.StringType, Key: key, String: val}
636640
}

0 commit comments

Comments
 (0)