Skip to content

Commit 73c7d9a

Browse files
committed
refactor: replace logger
This replaces the zap logging engine with Go 1.21's new structured logger, log/slog, or more precisely a thin wrapper around that ("xlog"). The log/slog package has a few things missing, which are present in xlog: 1. xlog provides a no-op logger, which simply discards any log output. This is extensively used in our tests. 2. xlog has a Fatal() output method, which simply calls Error() and then os.Exit(1). 3. xlog treats error values as first-class citizen. Since (log/slog).Error() is a convenience function for their default logger instance, there is no built-in way to treat errors as values. In comparison, (xlog).Error() constructs an slog.Attr, since xlog does not provide a default logger. Point (2) is debatable, since xlog.Fatal is only used in cmd/texd/main.go, so I'd be willing to forfeit it. Some TODOs remain: - xlog, i.e. its extension over log/slog, is not really tested - documentation is missing - the current xlog constructor (New) is a bit clunky to use, maybe switch to functional options? - some tests create a local buffer as log output target - this could be made easier with a `log, buf := xlog.NewBuffered()` constructor (albeit, the overhead is minimal) - for local development, I still like to have some colorization Issue: #1
1 parent 5712300 commit 73c7d9a

32 files changed

+369
-247
lines changed

cmd/texd/main.go

+39-48
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"log/slog"
78
"os"
89
"os/signal"
910
"runtime"
1011
"runtime/debug"
12+
"strings"
1113
"sync"
1214
"syscall"
1315
"time"
@@ -20,11 +22,10 @@ import (
2022
"github.com/digineo/texd/refstore/nop"
2123
"github.com/digineo/texd/service"
2224
"github.com/digineo/texd/tex"
25+
"github.com/digineo/texd/xlog"
2326
"github.com/docker/go-units"
2427
"github.com/spf13/pflag"
2528
"github.com/thediveo/enumflag"
26-
"go.uber.org/zap"
27-
"go.uber.org/zap/zapcore"
2829
)
2930

3031
var opts = service.Options{
@@ -42,7 +43,7 @@ var (
4243
engine = tex.DefaultEngine.Name()
4344
jobdir = ""
4445
pull = false
45-
logLevel = zapcore.InfoLevel.String()
46+
logLevel = slog.LevelInfo.String()
4647
maxJobSize = units.BytesSize(float64(opts.MaxJobSize))
4748
storageDSN = ""
4849
showVersion = false
@@ -108,7 +109,7 @@ func parseFlags() []string {
108109
fmt.Sprintf("enable reference store and configure with `DSN`, available adapters are: %v", refstore.AvailableAdapters()))
109110
fs.BoolVar(&pull, "pull", pull, "always pull Docker images")
110111
fs.StringVar(&logLevel, "log-level", logLevel,
111-
"set logging verbosity, acceptable values are: [debug, info, warn, error, dpanic, panic, fatal]")
112+
"set logging verbosity, acceptable values are: [debug, info, warn, error, fatal]")
112113
fs.BoolVarP(&showVersion, "version", "v", showVersion,
113114
`print version information and exit`)
114115

@@ -137,8 +138,10 @@ func parseFlags() []string {
137138
func main() {
138139
texd.PrintBanner(os.Stdout)
139140
images := parseFlags() //nolint:ifshort // func has sideeffects
140-
log, sync := setupLogger()
141-
defer sync()
141+
log, err := setupLogger()
142+
if err != nil {
143+
panic(err)
144+
}
142145

143146
if showVersion {
144147
printVersion()
@@ -147,32 +150,32 @@ func main() {
147150

148151
if err := tex.SetJobBaseDir(jobdir); err != nil {
149152
log.Fatal("error setting job directory",
150-
zap.String("flag", "--job-directory"),
151-
zap.Error(err))
153+
xlog.String("flag", "--job-directory"),
154+
xlog.Error(err))
152155
}
153156
if err := tex.SetDefaultEngine(engine); err != nil {
154157
log.Fatal("error setting default TeX engine",
155-
zap.String("flag", "--tex-engine"),
156-
zap.Error(err))
158+
xlog.String("flag", "--tex-engine"),
159+
xlog.Error(err))
157160
}
158161
if max, err := units.FromHumanSize(maxJobSize); err != nil {
159162
log.Fatal("error parsing maximum job size",
160-
zap.String("flag", "--max-job-size"),
161-
zap.Error(err))
163+
xlog.String("flag", "--max-job-size"),
164+
xlog.Error(err))
162165
} else {
163166
opts.MaxJobSize = max
164167
}
165168
if storageDSN != "" {
166169
rp, err := retentionPolicy()
167170
if err != nil {
168171
log.Fatal("error initializing retention policy",
169-
zap.String("flag", "--retention-policy, and/or --rp-access-items, --rp-access-size"),
170-
zap.Error(err))
172+
xlog.String("flag", "--retention-policy, and/or --rp-access-items, --rp-access-size"),
173+
xlog.Error(err))
171174
}
172175
if adapter, err := refstore.NewStore(storageDSN, rp); err != nil {
173176
log.Fatal("error parsing reference store DSN",
174-
zap.String("flag", "--reference-store"),
175-
zap.Error(err))
177+
xlog.String("flag", "--reference-store"),
178+
xlog.Error(err))
176179
} else {
177180
opts.RefStore = adapter
178181
}
@@ -181,23 +184,25 @@ func main() {
181184
}
182185

183186
if len(images) > 0 {
184-
log.Info("using docker", zap.Strings("images", images))
187+
log.Info("using docker", xlog.String("images", strings.Join(images, ",")))
185188
cli, err := exec.NewDockerClient(log, tex.JobBaseDir())
186189
if err != nil {
187-
log.Fatal("error connecting to dockerd", zap.Error(err))
190+
log.Error("error connecting to dockerd", xlog.Error(err))
191+
os.Exit(1)
188192
}
189193

190194
opts.Images, err = cli.SetImages(context.Background(), pull, images...)
191195
opts.Mode = "container"
192196
if err != nil {
193-
log.Fatal("error setting images", zap.Error(err))
197+
log.Error("error setting images", xlog.Error(err))
198+
os.Exit(1)
194199
}
195200
opts.Executor = cli.Executor
196201
}
197202

198203
stop, err := service.Start(opts, log)
199204
if err != nil {
200-
log.Fatal("failed to start service", zap.Error(err))
205+
log.Fatal("failed to start service", xlog.Error(err))
201206
}
202207
onExit(log, stop)
203208
}
@@ -206,14 +211,14 @@ const exitTimeout = 10 * time.Second
206211

207212
type stopFun func(context.Context) error
208213

209-
func onExit(log *zap.Logger, stopper ...stopFun) {
214+
func onExit(log xlog.Logger, stopper ...stopFun) {
210215
exitCh := make(chan os.Signal, 2)
211216
signal.Notify(exitCh, syscall.SIGINT, syscall.SIGTERM)
212217
sig := <-exitCh
213218

214219
log.Info("performing shutdown, press Ctrl+C to exit now",
215-
zap.String("signal", sig.String()),
216-
zap.Duration("graceful-wait-timeout", exitTimeout))
220+
xlog.String("signal", sig.String()),
221+
slog.Duration("graceful-wait-timeout", exitTimeout))
217222

218223
ctx, cancel := context.WithTimeout(context.Background(), exitTimeout)
219224
defer cancel()
@@ -223,7 +228,7 @@ func onExit(log *zap.Logger, stopper ...stopFun) {
223228
for _, stop := range stopper {
224229
go func(f stopFun) {
225230
if err := f(ctx); err != nil {
226-
log.Error("error while shutting down", zap.Error(err))
231+
log.Error("error while shutting down", xlog.Error(err))
227232
}
228233
wg.Done()
229234
}(stop)
@@ -266,34 +271,20 @@ func printVersion() {
266271
}
267272
}
268273

269-
func setupLogger() (*zap.Logger, func()) {
270-
var cfg zap.Config
271-
if texd.Development() {
272-
cfg = zap.NewDevelopmentConfig()
273-
} else {
274-
cfg = zap.NewProductionConfig()
275-
}
276-
277-
lvl, lvlErr := zapcore.ParseLevel(logLevel)
278-
if lvlErr == nil {
279-
cfg.Level = zap.NewAtomicLevelAt(lvl)
280-
}
281-
282-
log, err := cfg.Build()
274+
func setupLogger() (xlog.Logger, error) {
275+
lvl, err := xlog.ParseLevel(logLevel)
283276
if err != nil {
284-
// we don't have a logger yet, so logging the error
285-
// proves to be complicated :)
286-
panic(err)
277+
return nil, err
287278
}
288279

289-
if lvlErr != nil {
290-
log.Error("error parsing log level",
291-
zap.String("flag", "--log-level"),
292-
zap.Error(lvlErr))
280+
o := &slog.HandlerOptions{
281+
AddSource: true,
282+
// XXX: provide ReplaceAttr callback to normalize Source locations?
283+
Level: lvl,
293284
}
294285

295-
zap.ReplaceGlobals(log)
296-
return log, func() {
297-
_ = log.Sync()
286+
if texd.Development() {
287+
return xlog.New(xlog.TypeText, os.Stderr, o)
298288
}
289+
return xlog.New(xlog.TypeJSON, os.Stdout, o)
299290
}

exec/docker.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"context"
55

66
"github.com/digineo/texd/tex"
7-
"go.uber.org/zap"
7+
"github.com/digineo/texd/xlog"
88
)
99

1010
type dockerRunner interface {
@@ -23,17 +23,17 @@ func (dc *DockerClient) Executor(doc Document) Exec {
2323
}
2424
}
2525

26-
func (x *dockerExec) Run(ctx context.Context, log *zap.Logger) error {
26+
func (x *dockerExec) Run(ctx context.Context, log xlog.Logger) error {
2727
dir, cmd, err := x.extract()
2828
if err != nil {
2929
return tex.CompilationError("invalid document", err, nil)
3030
}
3131

3232
tag := x.doc.Image()
33-
log.Debug("running latexmk", zap.Strings("args", cmd[1:]))
33+
log.Debug("running latexmk", xlog.Any("args", cmd[1:]))
3434
output, err := x.cli.Run(ctx, tag, dir, cmd)
3535
if err != nil {
36-
log.Error("compilation failed", zap.Error(err))
36+
log.Error("compilation failed", xlog.Error(err))
3737
return tex.CompilationError("compilation failed", err, tex.KV{
3838
"cmd": cmd[0],
3939
"args": cmd[1:],

exec/docker_client.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99

1010
"github.com/digineo/texd/service/middleware"
11+
"github.com/digineo/texd/xlog"
1112
"github.com/docker/docker/api/types"
1213
"github.com/docker/docker/api/types/container"
1314
"github.com/docker/docker/api/types/image"
@@ -16,7 +17,6 @@ import (
1617
"github.com/docker/docker/pkg/jsonmessage"
1718
"github.com/docker/docker/pkg/stdcopy"
1819
"github.com/moby/term"
19-
"go.uber.org/zap"
2020
)
2121

2222
// newClient is swapped in tests
@@ -27,7 +27,7 @@ var newClient = func() (client.APIClient, error) {
2727
// DockerClient wraps a Docker client instance and provides methods to
2828
// pull images and start containers.
2929
type DockerClient struct {
30-
log *zap.Logger
30+
log xlog.Logger
3131
cli client.APIClient
3232
images []image.Summary
3333

@@ -41,14 +41,14 @@ type DockerClient struct {
4141
// When running in a Docker-in-Docker environment, baseDir is used to
4242
// determine the volume path on the Docker host, in order to mount
4343
// job directories correctly.
44-
func NewDockerClient(log *zap.Logger, baseDir string) (h *DockerClient, err error) {
44+
func NewDockerClient(log xlog.Logger, baseDir string) (h *DockerClient, err error) {
4545
cli, err := newClient()
4646
if err != nil {
4747
return nil, err
4848
}
4949

5050
if log == nil {
51-
log = zap.NewNop()
51+
log = xlog.NewNop()
5252
}
5353
dc := &DockerClient{
5454
log: log,
@@ -83,14 +83,14 @@ func (dc *DockerClient) SetImages(ctx context.Context, alwaysPull bool, tags ...
8383
if img.ID == "" || alwaysPull {
8484
toPull = append(toPull, tag)
8585
} else {
86-
dc.log.Info("image already present", zap.String("image", tag))
86+
dc.log.Info("image already present", xlog.String("image", tag))
8787
knownImages = append(knownImages, img)
8888
}
8989
}
9090

9191
p := newProgressReporter(os.Stdout)
9292
for _, tag := range toPull {
93-
dc.log.Info("pulling missing image", zap.String("image", tag))
93+
dc.log.Info("pulling missing image", xlog.String("image", tag))
9494
if err := dc.pull(ctx, tag, p); err != nil {
9595
return nil, err
9696
}
@@ -210,8 +210,8 @@ func (dc *DockerClient) prepareContainer(ctx context.Context, tag, wd string, cm
210210
}
211211
dc.log.Debug("container is ready",
212212
middleware.RequestIDField(ctx),
213-
zap.String("id", worker.ID),
214-
zap.String("work-dir", wd))
213+
xlog.String("id", worker.ID),
214+
xlog.String("work-dir", wd))
215215
return worker.ID, nil
216216
}
217217

exec/docker_client_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"syscall"
1313
"testing"
1414

15+
"github.com/digineo/texd/xlog"
1516
"github.com/docker/docker/api/types"
1617
"github.com/docker/docker/api/types/container"
1718
"github.com/docker/docker/api/types/image"
@@ -22,7 +23,6 @@ import (
2223
"github.com/spf13/afero"
2324
"github.com/stretchr/testify/mock"
2425
"github.com/stretchr/testify/suite"
25-
"go.uber.org/zap"
2626
)
2727

2828
// bg is used as default context given to the apiMock stubs.
@@ -115,7 +115,7 @@ func (s *dockerClientSuite) SetupTest() {
115115
s.cli = &apiMock{}
116116
s.subject = &DockerClient{
117117
cli: s.cli,
118-
log: zap.NewNop(),
118+
log: xlog.NewNop(),
119119
}
120120
}
121121

exec/docker_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import (
77
"testing"
88

99
"github.com/digineo/texd/tex"
10+
"github.com/digineo/texd/xlog"
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/mock"
1213
"github.com/stretchr/testify/require"
13-
"go.uber.org/zap"
1414
)
1515

1616
type dockerClientMock struct {
@@ -24,7 +24,7 @@ func (m *dockerClientMock) Run(ctx context.Context, tag, wd string, cmd []string
2424

2525
func TestDockerClient_Executor(t *testing.T) {
2626
subject := (&DockerClient{
27-
log: zap.NewNop(),
27+
log: xlog.NewNop(),
2828
cli: &apiMock{},
2929
}).Executor(&mockDocument{})
3030
require.NotNil(t, subject)
@@ -37,7 +37,7 @@ func TestDockerExec_invalidDocument(t *testing.T) {
3737
cli: nil, // not accessed
3838
}
3939

40-
err := exec.Run(bg, zap.NewNop())
40+
err := exec.Run(bg, xlog.NewNop())
4141
require.EqualError(t, err, "invalid document: "+io.ErrClosedPipe.Error())
4242
}
4343

@@ -55,7 +55,7 @@ func TestDockerExec_latexmkFailed(t *testing.T) {
5555
cli: cli,
5656
}
5757

58-
err := exec.Run(bg, zap.NewNop())
58+
err := exec.Run(bg, xlog.NewNop())
5959
require.EqualError(t, err, "compilation failed: "+errStart.Error())
6060
assert.True(t, tex.IsCompilationError(err))
6161

@@ -78,6 +78,6 @@ func TestDockerExec_success(t *testing.T) {
7878
cli: cli,
7979
}
8080

81-
err := exec.Run(bg, zap.NewNop())
81+
err := exec.Run(bg, xlog.NewNop())
8282
require.NoError(t, err)
8383
}

exec/exec.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"context"
55

66
"github.com/digineo/texd/tex"
7-
"go.uber.org/zap"
7+
"github.com/digineo/texd/xlog"
88
)
99

1010
type Exec interface {
11-
Run(context.Context, *zap.Logger) error
11+
Run(context.Context, xlog.Logger) error
1212
}
1313

1414
// Document is a sub-set of the tex.Document interface.

0 commit comments

Comments
 (0)