Jelajahi Sumber

Refactor to migrate from sirupsen/logrus to rs/zerolog and integrate DeRuina/timberjack for logfile rotation

Fixes #228
Bernhard Froehlich 1 bulan lalu
induk
melakukan
17421a539e
5 mengubah file dengan 205 tambahan dan 154 penghapusan
  1. 43 34
      config.go
  2. 4 4
      go.mod
  3. 18 16
      go.sum
  4. 50 25
      logger.go
  5. 90 75
      main.go

+ 43 - 34
config.go

@@ -12,7 +12,6 @@ import (
 	"time"
 
 	"github.com/peterbourgon/ff/v3"
-	"github.com/sirupsen/logrus"
 )
 
 var (
@@ -70,18 +69,19 @@ func setupAllowedNetworks() {
 	for _, netstr := range splitstr(*allowedNetsStr, ' ') {
 		baseIP, allowedNet, err := net.ParseCIDR(netstr)
 		if err != nil {
-			log.WithField("netstr", netstr).
-				WithError(err).
-				Fatal("Invalid CIDR notation in allowed_nets")
+			log.Fatal().
+				Str("netstr", netstr).
+				Err(err).
+				Msg("Invalid CIDR notation in allowed_nets")
 		}
 
 		// Reject any network specification where any host bits are set,
 		// meaning the address refers to a host and not a network.
 		if !allowedNet.IP.Equal(baseIP) {
-			log.WithFields(logrus.Fields{
-				"given_net":  netstr,
-				"proper_net": allowedNet,
-			}).Fatal("Invalid network in allowed_nets (host bits set)")
+			log.Fatal().
+				Str("given_net", netstr).
+				Str("proper_net", allowedNet.String()).
+				Msg("Invalid network in allowed_nets (host bits set)")
 		}
 
 		allowedNets = append(allowedNets, allowedNet)
@@ -94,30 +94,32 @@ func setupAllowedPatterns() {
 	if *allowedSenderStr != "" {
 		allowedSender, err = regexp.Compile(*allowedSenderStr)
 		if err != nil {
-			log.WithField("allowed_sender", *allowedSenderStr).
-				WithError(err).
-				Fatal("allowed_sender pattern invalid")
+			log.Fatal().
+				Str("allowed_sender", *allowedSenderStr).
+				Err(err).
+				Msg("allowed_sender pattern invalid")
 		}
 	}
 
 	if *allowedRecipStr != "" {
 		allowedRecipients, err = regexp.Compile(*allowedRecipStr)
 		if err != nil {
-			log.WithField("allowed_recipients", *allowedRecipStr).
-				WithError(err).
-				Fatal("allowed_recipients pattern invalid")
+			log.Fatal().
+				Str("allowed_recipients", *allowedRecipStr).
+				Err(err).
+				Msg("allowed_recipients pattern invalid")
 		}
 	}
 }
 
 func setupRemotes() {
-	logger := log.WithField("remotes", *remotesStr)
+	logger := log.With().Str("remotes", *remotesStr).Logger()
 
 	if *remotesStr != "" {
 		for _, remoteURL := range strings.Split(*remotesStr, " ") {
 			r, err := ParseRemote(remoteURL)
 			if err != nil {
-				logger.Fatal(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
+				logger.Fatal().Msg(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
 			}
 
 			remotes = append(remotes, r)
@@ -148,8 +150,9 @@ func setupListeners() {
 		pa := splitProto(listenAddr)
 
 		if localAuthRequired() && pa.protocol == "" {
-			log.WithField("address", pa.address).
-				Fatal("Local authentication (via allowed_users file) " +
+			log.Fatal().
+				Str("address", pa.address).
+				Msg("Local authentication (via allowed_users file) " +
 					"not allowed with non-TLS listener")
 		}
 
@@ -162,35 +165,41 @@ func setupTimeouts() {
 
 	readTimeout, err = time.ParseDuration(*readTimeoutStr)
 	if err != nil {
-		log.WithField("read_timeout", *readTimeoutStr).
-			WithError(err).
-			Fatal("read_timeout duration string invalid")
+		log.Fatal().
+			Str("read_timeout", *readTimeoutStr).
+			Err(err).
+			Msg("read_timeout duration string invalid")
 	}
 	if readTimeout.Seconds() < 1 {
-		log.WithField("read_timeout", *readTimeoutStr).
-			Fatal("read_timeout less than one second")
+		log.Fatal().
+			Str("read_timeout", *readTimeoutStr).
+			Msg("read_timeout less than one second")
 	}
 
 	writeTimeout, err = time.ParseDuration(*writeTimeoutStr)
 	if err != nil {
-		log.WithField("write_timeout", *writeTimeoutStr).
-			WithError(err).
-			Fatal("write_timeout duration string invalid")
+		log.Fatal().
+			Str("write_timeout", *writeTimeoutStr).
+			Err(err).
+			Msg("write_timeout duration string invalid")
 	}
 	if writeTimeout.Seconds() < 1 {
-		log.WithField("write_timeout", *writeTimeoutStr).
-			Fatal("write_timeout less than one second")
+		log.Fatal().
+			Str("write_timeout", *writeTimeoutStr).
+			Msg("write_timeout less than one second")
 	}
 
 	dataTimeout, err = time.ParseDuration(*dataTimeoutStr)
 	if err != nil {
-		log.WithField("data_timeout", *dataTimeoutStr).
-			WithError(err).
-			Fatal("data_timeout duration string invalid")
+		log.Fatal().
+			Str("data_timeout", *dataTimeoutStr).
+			Err(err).
+			Msg("data_timeout duration string invalid")
 	}
 	if dataTimeout.Seconds() < 1 {
-		log.WithField("data_timeout", *dataTimeoutStr).
-			Fatal("data_timeout less than one second")
+		log.Fatal().
+			Str("data_timeout", *dataTimeoutStr).
+			Msg("data_timeout less than one second")
 	}
 }
 
@@ -226,7 +235,7 @@ func ConfigLoad() {
 	}
 
 	if *remotesStr == "" && *command == "" {
-		log.Warn("no remotes or command set; mail will not be forwarded!")
+		log.Warn().Msg("no remotes or command set; mail will not be forwarded!")
 	}
 
 	setupAllowedNetworks()

+ 4 - 4
go.mod

@@ -1,19 +1,19 @@
 module github.com/decke/smtprelay
 
 require (
+	github.com/DeRuina/timberjack v1.3.3
 	github.com/chrj/smtpd v0.3.1
 	github.com/google/uuid v1.6.0
 	github.com/peterbourgon/ff/v3 v3.4.0
-	github.com/sirupsen/logrus v1.9.3
+	github.com/rs/zerolog v1.34.0
 	github.com/stretchr/testify v1.10.0
 	golang.org/x/crypto v0.40.0
 )
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	golang.org/x/sys v0.34.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
 go 1.24.3

+ 18 - 16
go.sum

@@ -1,27 +1,29 @@
+github.com/DeRuina/timberjack v1.3.3 h1:R+3oEvJP1wd8iYkGGsnq9e6pHGAXFqizPKReEMj0jvQ=
+github.com/DeRuina/timberjack v1.3.3/go.mod h1:pbNOvT1AIUxBqiH/ytL9nNsKrHUrDGJdz7T23y79RRU=
 github.com/chrj/smtpd v0.3.1 h1:kogHFkbFdKaoH3bgZkqNC9uVtKYOFfM3uV3rroBdooE=
 github.com/chrj/smtpd v0.3.1/go.mod h1:JtABvV/LzvLmEIzy0NyDnrfMGOMd8wy5frAokwf6J9Q=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
 github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
+github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
 golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 50 - 25
logger.go

@@ -2,59 +2,84 @@ package main
 
 import (
 	"fmt"
+	"io"
 	"os"
+	"strings"
 	"time"
 
-	"github.com/sirupsen/logrus"
+	"github.com/DeRuina/timberjack"
+	"github.com/rs/zerolog"
 )
 
 var (
-	log *logrus.Logger
+	rotator *timberjack.Logger
+	log     *zerolog.Logger
 )
 
 func setupLogger() {
-	log = logrus.New()
+	zerolog.TimeFieldFormat = time.RFC3339
 
 	// Handle logfile
+	var writer io.Writer
 	if *logFile == "" {
-		log.SetOutput(os.Stderr)
+		writer = os.Stderr
 	} else {
-		writer, err := os.OpenFile(*logFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)
-		if err != nil {
-			fmt.Printf("cannot open log file: %s\n", err)
-			os.Exit(1)
+		rotator = &timberjack.Logger{
+			Filename:         *logFile,
+			MaxSize:          10, // megabytes before rotation
+			MaxBackups:       3,
+			MaxAge:           30, // days
+			Compress:         true,
+			BackupTimeFormat: "20060102150405",
 		}
-
-		log.SetOutput(writer)
+		writer = rotator
 	}
 
 	// Handle log_format
 	switch *logFormat {
 	case "json":
-		log.SetFormatter(&logrus.JSONFormatter{
-			TimestampFormat:   time.RFC3339Nano,
-			DisableHTMLEscape: true,
-		})
+		// zerolog default is JSON
 	case "plain":
-		log.SetFormatter(&logrus.TextFormatter{
-			DisableTimestamp: true,
-		})
+		writer = zerolog.ConsoleWriter{
+			Out:        writer,
+			NoColor:    true,
+			TimeFormat: "",
+			FormatTimestamp: func(i interface{}) string {
+				return "" // avoid default time
+			},
+		}
 	case "", "default":
-		log.SetFormatter(&logrus.TextFormatter{
-			FullTimestamp: true,
-		})
+		writer = zerolog.ConsoleWriter{
+			Out:        writer,
+			NoColor:    true,
+			TimeFormat: time.RFC3339,
+		}
+	case "pretty":
+		writer = zerolog.ConsoleWriter{
+			Out:        writer,
+			TimeFormat: time.RFC3339Nano,
+		}
 	default:
 		fmt.Fprintf(os.Stderr, "Invalid log_format: %s\n", *logFormat)
 		os.Exit(1)
 	}
 
+	l := zerolog.New(writer).With().Timestamp().Logger()
+	log = &l
+
 	// Handle log_level
-	level, err := logrus.ParseLevel(*logLevel)
+	level, err := zerolog.ParseLevel(strings.ToLower(*logLevel))
 	if err != nil {
-		level = logrus.InfoLevel
+		level = zerolog.InfoLevel
+		log.Warn().Str("given_level", *logLevel).
+			Msg("could not parse log level, defaulting to 'info'")
+	}
+	zerolog.SetGlobalLevel(level)
+}
 
-		log.WithField("given_level", *logLevel).
-			Warn("could not parse log level, defaulting to 'info'")
+// Call this on shutdown if you want to close the rotator and stop timers cleanly
+func closeLogger() {
+	if rotator != nil {
+		rotator.Close()
 	}
-	log.SetLevel(level)
 }

+ 90 - 75
main.go

@@ -14,7 +14,6 @@ import (
 
 	"github.com/chrj/smtpd"
 	"github.com/google/uuid"
-	"github.com/sirupsen/logrus"
 )
 
 func connectionChecker(peer smtpd.Peer) error {
@@ -32,9 +31,9 @@ func connectionChecker(peer smtpd.Peer) error {
 		}
 	}
 
-	log.WithFields(logrus.Fields{
-		"ip": peerIP,
-	}).Warn("Connection refused from address outside of allowed_nets")
+	log.Warn().
+		Str("ip", peerIP.String()).
+		Msg("Connection refused from address outside of allowed_nets")
 	return smtpd.Error{Code: 421, Message: "Denied"}
 }
 
@@ -87,19 +86,21 @@ func senderChecker(peer smtpd.Peer, addr string) error {
 		user, err := AuthFetch(peer.Username)
 		if err != nil {
 			// Shouldn't happen: authChecker already validated username+password
-			log.WithFields(logrus.Fields{
-				"peer":     peer.Addr,
-				"username": peer.Username,
-			}).WithError(err).Warn("could not fetch auth user")
+			log.Warn().
+				Str("peer", peer.Addr.String()).
+				Str("username", peer.Username).
+				Err(err).
+				Msg("could not fetch auth user")
 			return smtpd.Error{Code: 451, Message: "Bad sender address"}
 		}
 
 		if !addrAllowed(addr, user.allowedAddresses) {
-			log.WithFields(logrus.Fields{
-				"peer":           peer.Addr,
-				"username":       peer.Username,
-				"sender_address": addr,
-			}).Warn("sender address not allowed for authenticated user")
+			log.Warn().
+				Str("peer", peer.Addr.String()).
+				Str("username", peer.Username).
+				Str("sender_address", addr).
+				Err(err).
+				Msg("sender address not allowed for authenticated user")
 			return smtpd.Error{Code: 451, Message: "Bad sender address"}
 		}
 	}
@@ -114,10 +115,10 @@ func senderChecker(peer smtpd.Peer, addr string) error {
 		return nil
 	}
 
-	log.WithFields(logrus.Fields{
-		"sender_address": addr,
-		"peer":           peer.Addr,
-	}).Warn("sender address not allowed by allowed_sender pattern")
+	log.Warn().
+		Str("sender_address", addr).
+		Str("peer", peer.Addr.String()).
+		Msg("sender address not allowed by allowed_sender pattern")
 	return smtpd.Error{Code: 451, Message: "Bad sender address"}
 }
 
@@ -132,20 +133,21 @@ func recipientChecker(peer smtpd.Peer, addr string) error {
 		return nil
 	}
 
-	log.WithFields(logrus.Fields{
-		"peer":              peer.Addr,
-		"recipient_address": addr,
-	}).Warn("recipient address not allowed by allowed_recipients pattern")
+	log.Warn().
+		Str("peer", peer.Addr.String()).
+		Str("recipient_address", addr).
+		Msg("recipient address not allowed by allowed_recipients pattern")
 	return smtpd.Error{Code: 451, Message: "Bad recipient address"}
 }
 
 func authChecker(peer smtpd.Peer, username string, password string) error {
 	err := AuthCheckPassword(username, password)
 	if err != nil {
-		log.WithFields(logrus.Fields{
-			"peer":     peer.Addr,
-			"username": username,
-		}).WithError(err).Warn("auth error")
+		log.Warn().
+			Str("peer", peer.Addr.String()).
+			Str("username", username).
+			Err(err).
+			Msg("auth error")
 		return smtpd.Error{Code: 535, Message: "Authentication credentials invalid"}
 	}
 	return nil
@@ -157,12 +159,12 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
 		peerIP = addr.IP.String()
 	}
 
-	logger := log.WithFields(logrus.Fields{
-		"from": env.Sender,
-		"to":   env.Recipients,
-		"peer": peerIP,
-		"uuid": generateUUID(),
-	})
+	logger := log.With().
+		Str("from", env.Sender).
+		Strs("to", env.Recipients).
+		Str("peer", peerIP).
+		Str("uuid", generateUUID()).
+		Logger()
 
 	var envRemotes []*Remote
 
@@ -177,14 +179,14 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
 	}
 
 	if len(envRemotes) == 0 && *command == "" {
-		logger.Warning("no remote_host or command set; discarding mail")
+		logger.Warn().Msg("no remote_host or command set; discarding mail")
 		return smtpd.Error{Code: 554, Message: "There are no appropriate remote_host or command"}
 	}
 
 	env.AddReceivedLine(peer)
 
 	if *command != "" {
-		cmdLogger := logger.WithField("command", *command)
+		cmdLogger := logger.With().Str("command", *command).Logger()
 
 		var stdout bytes.Buffer
 		var stderr bytes.Buffer
@@ -205,16 +207,16 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
 
 		err := cmd.Run()
 		if err != nil {
-			cmdLogger.WithError(err).Error(stderr.String())
+			cmdLogger.Error().Err(err).Msg(stderr.String())
 			return smtpd.Error{Code: 554, Message: "External command failed"}
 		}
 
-		cmdLogger.Info("pipe command successful: " + stdout.String())
+		cmdLogger.Info().Msg("pipe command successful: " + stdout.String())
 	}
 
 	for _, remote := range envRemotes {
-		logger = logger.WithField("host", remote.Addr)
-		logger.Info("delivering mail from peer using smarthost")
+		logger = logger.With().Str("host", remote.Addr).Logger()
+		logger.Info().Msg("delivering mail from peer using smarthost")
 
 		err := SendMail(
 			remote,
@@ -229,21 +231,22 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
 			case *textproto.Error:
 				smtpError = smtpd.Error{Code: err.Code, Message: err.Msg}
 
-				logger.WithFields(logrus.Fields{
-					"err_code": err.Code,
-					"err_msg":  err.Msg,
-				}).Error("delivery failed")
+				logger.Error().
+					Int("err_code", err.Code).
+					Str("err_msg", err.Msg).
+					Msg("delivery failed")
 			default:
 				smtpError = smtpd.Error{Code: 421, Message: "Forwarding failed"}
 
-				logger.WithError(err).
-					Error("delivery failed")
+				logger.Error().
+					Err(err).
+					Msg("delivery failed")
 			}
 
 			return smtpError
 		}
 
-		logger.Debug("delivery successful")
+		logger.Debug().Msg("delivery successful")
 	}
 
 	return nil
@@ -253,8 +256,9 @@ func generateUUID() string {
 	uniqueID, err := uuid.NewRandom()
 
 	if err != nil {
-		log.WithError(err).
-			Error("could not generate UUIDv4")
+		log.Error().
+			Err(err).
+			Msg("could not generate UUIDv4")
 
 		return ""
 	}
@@ -280,16 +284,17 @@ func getTLSConfig() *tls.Config {
 	}
 
 	if *localCert == "" || *localKey == "" {
-		log.WithFields(logrus.Fields{
-			"cert_file": *localCert,
-			"key_file":  *localKey,
-		}).Fatal("TLS certificate/key file not defined in config")
+		log.Fatal().
+			Str("cert_file", *localCert).
+			Str("key_file", *localKey).
+			Msg("TLS certificate/key file not defined in config")
 	}
 
 	cert, err := tls.LoadX509KeyPair(*localCert, *localKey)
 	if err != nil {
-		log.WithField("error", err).
-			Fatal("cannot load X509 keypair")
+		log.Fatal().
+			Err(err).
+			Msg("cannot load X509 keypair")
 	}
 
 	return &tls.Config{
@@ -303,16 +308,18 @@ func getTLSConfig() *tls.Config {
 func main() {
 	ConfigLoad()
 
-	log.WithField("version", appVersion).
-		Debug("starting smtprelay")
+	log.Debug().
+		Str("version", appVersion).
+		Msg("starting smtprelay")
 
 	// Load allowed users file
 	if localAuthRequired() {
 		err := AuthLoadFile(*allowedUsers)
 		if err != nil {
-			log.WithField("file", *allowedUsers).
-				WithError(err).
-				Fatal("cannot load allowed users file")
+			log.Fatal().
+				Str("file", *allowedUsers).
+				Err(err).
+				Msg("cannot load allowed users file")
 		}
 	}
 
@@ -320,7 +327,7 @@ func main() {
 
 	// Create a server for each desired listen address
 	for _, listen := range listenAddrs {
-		logger := log.WithField("address", listen.address)
+		logger := log.With().Str("address", listen.address).Logger()
 
 		server := &smtpd.Server{
 			Hostname:          *hostName,
@@ -346,29 +353,32 @@ func main() {
 
 		switch listen.protocol {
 		case "":
-			logger.Info("listening on address")
+			logger.Info().Msg("listening on address")
 			lsnr, err = net.Listen("tcp", listen.address)
 
 		case "starttls":
 			server.TLSConfig = getTLSConfig()
 			server.ForceTLS = *localForceTLS
 
-			logger.Info("listening on address (STARTTLS)")
+			logger.Info().Msg("listening on address (STARTTLS)")
 			lsnr, err = net.Listen("tcp", listen.address)
 
 		case "tls":
 			server.TLSConfig = getTLSConfig()
 
-			logger.Info("listening on address (TLS)")
+			logger.Info().Msg("listening on address (TLS)")
 			lsnr, err = tls.Listen("tcp", listen.address, server.TLSConfig)
 
 		default:
-			logger.WithField("protocol", listen.protocol).
-				Fatal("unknown protocol in listen address")
+			logger.Fatal().
+				Str("protocol", listen.protocol).
+				Msg("unknown protocol in listen address")
 		}
 
 		if err != nil {
-			logger.WithError(err).Fatal("error starting listener")
+			logger.Fatal().
+				Err(err).
+				Msg("error starting listener")
 		}
 		servers = append(servers, server)
 
@@ -381,27 +391,31 @@ func main() {
 
 	// First close the listeners
 	for _, server := range servers {
-		logger := log.WithField("address", server.Address())
-		logger.Debug("Shutting down server")
+		logger := log.With().Str("address", server.Address().String()).Logger()
+		logger.Debug().Msg("Shutting down server")
 		err := server.Shutdown(false)
 		if err != nil {
-			logger.WithError(err).
-				Warning("Shutdown failed")
+			logger.Warn().
+				Err(err).
+				Msg("Shutdown failed")
 		}
 	}
 
 	// Then wait for the clients to exit
 	for _, server := range servers {
-		logger := log.WithField("address", server.Address())
-		logger.Debug("Waiting for server")
+		logger := log.With().Str("address", server.Address().String()).Logger()
+		logger.Debug().Msg("Waiting for server")
 		err := server.Wait()
 		if err != nil {
-			logger.WithError(err).
-				Warning("Wait failed")
+			logger.Warn().
+				Err(err).
+				Msg("Wait failed")
 		}
 	}
 
-	log.Debug("done")
+	log.Debug().Msg("done")
+
+	closeLogger()
 }
 
 func handleSignals() {
@@ -410,6 +424,7 @@ func handleSignals() {
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
 	sig := <-sigs
 
-	log.WithField("signal", sig).
-		Info("shutting down in response to received signal")
+	log.Info().
+		Str("signal", sig.String()).
+		Msg("shutting down in response to received signal")
 }