diff --git a/cmd/menshen/main.go b/cmd/menshen/main.go
index 2e1a6e7e2b17326d459eda92ef565392d18835a8..b920ce5aa17ccfe82ae4b2c68791bbd4ff6131a7 100644
--- a/cmd/menshen/main.go
+++ b/cmd/menshen/main.go
@@ -6,8 +6,8 @@ import (
 	"os"
 	"strings"
 
-	"github.com/apex/log"
-	cliHandler "github.com/apex/log/handlers/cli"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
 
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
@@ -89,9 +89,16 @@ func main() {
 
 	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
 	pflag.Parse()
+
+	consoleWriter := zerolog.ConsoleWriter{
+		Out:        os.Stdout,
+		TimeFormat: "2006-01-02T15:04:05.999Z07:00",
+	}
+	log.Logger = zerolog.New(consoleWriter).With().Timestamp().Logger()
+
 	err := viper.BindPFlags(pflag.CommandLine)
 	if err != nil {
-		log.WithError(err).Fatal("Failed to BindPFlags")
+		log.Fatal().Msgf("Failed to BindPFlags: %s", err)
 	}
 
 	viper.AddConfigPath(".")
@@ -99,7 +106,7 @@ func main() {
 	viper.SetConfigType("yaml")
 	err = viper.ReadInConfig()
 	if err != nil {
-		log.WithError(err).Warn("Failed to ReadInConfig")
+		log.Debug().Msgf("Not using optional menshen.yaml: %s", err)
 	}
 
 	// allow to specify flags from environment variables too
@@ -134,7 +141,7 @@ func main() {
 		err = viper.BindEnv(verbose, "MENSHEN_VERBOSE")
 	}
 	if err != nil {
-		log.WithError(err).Fatal("Failed to BindEnv")
+		log.Fatal().Msgf("Failed to BindEnv: %s", err)
 	}
 
 	// for the few single-word flags at least
@@ -144,9 +151,8 @@ func main() {
 
 	verbose := viper.GetBool(verbose)
 	if verbose {
-		log.SetLevel(log.DebugLevel)
+		zerolog.SetGlobalLevel(zerolog.DebugLevel)
 	}
-	log.SetHandler(cliHandler.New(os.Stdout))
 
 	lbAddr := viper.GetString(addrLoadBalancer)
 
@@ -178,70 +184,68 @@ func main() {
 	}
 
 	if cfg.EIPURL != "" {
-		log.Infof("Getting EIP File from: %s", cfg.EIPURL)
+		log.Info().Msgf("Getting EIP File from %s", cfg.EIPURL)
 	}
 	if cfg.EIP != "" {
-		log.Infof("Getting EIP File from: %s", cfg.EIP)
+		log.Info().Msgf("Getting EIP File from %s", cfg.EIP)
 	}
 	if cfg.ProviderJson == "" {
-		log.Errorf("Error: parameter --%s is required.", fromProviderJsonFile)
-		log.Errorf("File path to provider.json is missing. Please specify a source by using --%s", fromProviderJsonFile)
+		log.Error().Msgf("Error: parameter --%s is required.", fromProviderJsonFile)
+		log.Error().Msgf("File path to provider.json is missing. Please specify a source by using --%s", fromProviderJsonFile)
 		os.Exit(1)
 	}
 
 	if cfg.EIPURL == "" && cfg.EIP == "" {
-		log.Errorf("Error: No gateways loaded. Please specify a " +
-			fmt.Sprintf("gateway source by using --%s or --%s", fromEIPURL, fromEIPFile))
+		log.Error().Msgf("No gateways loaded. Please specify a gateway source by using --%s or --%s", fromEIPURL, fromEIPFile)
 		os.Exit(1)
 	}
 
 	if cfg.CaFile == "" {
-		log.Errorf("Error: parameter %s is required", caFile)
+		log.Error().Msgf("parameter %s is required", caFile)
 		os.Exit(1)
 	} else if _, err := os.Stat(cfg.CaFile); err != nil {
-		log.Errorf("Error: Could not load CaFile. %s", err)
+		log.Error().Msgf("Could not load CaFile. %s", err)
 		os.Exit(1)
 	} else {
-		log.Debugf("Using %s as CaFile", cfg.CaFile)
+		log.Debug().Msgf("Using CaFile %s", cfg.CaFile)
 	}
 
 	// either clientcertURL or else cfg.OvpnCaCrt, cfg.OvpnCaKey, cfg.Algo are required for local cert generation
 	if cfg.ClientCertURL != "" {
-		log.Infof("Configuring menshen to fetch certs form remote URL: %s", cfg.ClientCertURL)
+		log.Info().Msgf("Configuring menshen to fetch certs form remote URL: %s", cfg.ClientCertURL)
 	} else {
 		if cfg.OvpnCaCrt == "" {
-			log.Errorf("Error: parameter --%s is required.", ovpnCaCrt)
-			log.Errorf("Please specify a file containing the CA certificate required for generating openvpn client certificate.")
+			log.Error().Msgf("Error: parameter --%s is required.", ovpnCaCrt)
+			log.Error().Msg("Please specify a file containing the CA certificate required for generating openvpn client certificate.")
 			os.Exit(1)
 		} else if _, err := os.Stat(cfg.OvpnCaCrt); err != nil {
-			log.Errorf("Error: Could not load %s. %s", ovpnCaCrt, err)
+			log.Error().Msgf("Error: Could not load %s. %s", ovpnCaCrt, err)
 			os.Exit(1)
 		} else {
-			log.Debug(fmt.Sprintf("Using %s as %s", cfg.OvpnCaCrt, ovpnCaCrt))
+			log.Debug().Msgf("Using %s as %s", cfg.OvpnCaCrt, ovpnCaCrt)
 		}
 
 		if cfg.OvpnCaKey == "" {
-			log.Errorf("Error: parameter --%s is required.", ovpnCaKey)
-			log.Errorf("Please specify a file containing the CA key required for signing openvpn client certificate.")
+			log.Error().Msgf("parameter --%s is required.", ovpnCaKey)
+			log.Error().Msg("Please specify a file containing the CA key required for signing openvpn client certificate.")
 			os.Exit(1)
 		} else if _, err := os.Stat(cfg.OvpnCaKey); err != nil {
-			log.Errorf("Error: Could not load %s. %s", ovpnCaKey, err)
+			log.Error().Msgf("Could not load %s. %s", ovpnCaKey, err)
 			os.Exit(1)
 		} else {
-			log.Debugf("Using %s as %s", cfg.OvpnCaKey, ovpnCaKey)
+			log.Debug().Msgf("Using %s as %s", cfg.OvpnCaKey, ovpnCaKey)
 		}
 
 		if cfg.Algo != "ed25519" && cfg.Algo != "ecdsa" && cfg.Algo != "rsa" {
-			log.Errorf("Error: parameter --%s %s is not supported.", algo, cfg.Algo)
-			log.Errorf("Please specify a supported algo for cert generation. Currently supported algorithms are: ed25519, ecdsa, rsa.")
+			log.Error().Msgf("Error: parameter --%s %s is not supported.", algo, cfg.Algo)
+			log.Error().Msg("Please specify a supported algo for cert generation. Currently supported algorithms are: ed25519, ecdsa, rsa.")
 			os.Exit(1)
 		}
 	}
 
 	echoMetrics := api.InitMetricsServer()
 	go func() {
-		msg := fmt.Sprintf("Starting /metrics server on :%d", cfg.PortMetrics)
-		log.Infof(msg)
+		log.Info().Msgf("Starting /metrics server on :%d", cfg.PortMetrics)
 		echoMetrics.Logger.Fatal(echoMetrics.Start(
 			fmt.Sprintf(":%d", cfg.PortMetrics)))
 	}()
@@ -254,7 +258,7 @@ func main() {
 	if cfg.AutoTLS {
 		e.Logger.Fatal(e.StartAutoTLS(portStr))
 	} else {
-		fmt.Println("starting:", portStr)
+		log.Info().Msgf("Starting backend: %s", portStr)
 		e.Logger.Fatal(e.Start(portStr))
 	}
 }
diff --git a/pkg/api/agent.go b/pkg/api/agent.go
index ad05e6382d4dd0d4613049a1c8bda6b9d5ff980a..230ca379877c355e4d9f89d44081bf1a2a3447d5 100644
--- a/pkg/api/agent.go
+++ b/pkg/api/agent.go
@@ -6,8 +6,8 @@ import (
 	"time"
 
 	"0xacab.org/leap/menshen/pkg/models"
-	"github.com/apex/log"
 	"github.com/labstack/echo/v4"
+	"github.com/rs/zerolog/log"
 )
 
 func bridgesMatch(bridge1, bridge2 models.Bridge) bool {
@@ -16,12 +16,12 @@ func bridgesMatch(bridge1, bridge2 models.Bridge) bool {
 	if len(bridge1.Options) > 0 {
 		bridge1OptionsBytes, err := json.Marshal(bridge1.Options)
 		if err != nil {
-			log.Errorf("Could not marshal bridge1 options. Bridge1: %#v, err: %v", err)
+			log.Warn().Msgf("Could not marshal bridge1 options. Bridge1: %#v, err: %v", bridge1, err)
 			return false
 		}
 		bridge2OptionsBytes, err := json.Marshal(bridge2.Options)
 		if err != nil {
-			log.Errorf("Could not marshal bridge2 options. Bridge2: %#v, err: %v", err)
+			log.Warn().Msgf("Could not marshal bridge2 options. Bridge2: %#v, err: %v", bridge2, err)
 			return false
 		}
 
@@ -73,7 +73,7 @@ func (r *registry) RegisterBridge(c echo.Context) error {
 	// TODO: consider validation?
 	err := c.Bind(&bridgeRequest)
 	if err != nil {
-		log.Errorf("failed to bind request body: %v", err)
+		log.Warn().Msgf("failed to bind request body: %v", err)
 		return c.JSON(http.StatusBadRequest, "bad request")
 	}
 
@@ -125,7 +125,7 @@ func (r *registry) RegisterGateway(c echo.Context) error {
 	// TODO: consider validation?
 	err := c.Bind(&gatewayRequest)
 	if err != nil {
-		log.Errorf("failed to bind request body: %v", err)
+		log.Warn().Msgf("failed to bind request body: %v", err)
 		return c.JSON(http.StatusBadRequest, "bad request")
 	}
 
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 27699b744e5923622d830d9d5e4078f28b0893f2..89e231f96af33bbfe1fd2b9da04624a78f9d26c7 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -4,10 +4,10 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/apex/log"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/rs/zerolog/log"
 	echoSwagger "github.com/swaggo/echo-swagger"
 	"golang.org/x/crypto/acme/autocert"
 
@@ -25,7 +25,7 @@ func InitMetricsServer() *echo.Echo {
 }
 
 func InitServer(cfg *Config) *echo.Echo {
-	log.Info("Initializing server...")
+	log.Info().Msg("Initializing server...")
 
 	if cfg.AutoTLS {
 		sw.SwaggerInfo.Host = cfg.ServerName
@@ -43,7 +43,7 @@ func InitServer(cfg *Config) *echo.Echo {
 
 	db, err := storage.OpenDatabase(cfg.DBFile)
 	if err != nil {
-		log.Fatalf("Error opening database: %v", err)
+		log.Fatal().Msgf("Error opening database: %v", err)
 	}
 
 	e.Use(storageMiddleware(db))
@@ -52,21 +52,34 @@ func InitServer(cfg *Config) *echo.Echo {
 
 	// We don't want no panics in production
 	//e.Use(middleware.Recover())
-	e.Use(middleware.Logger())
+	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+		LogStatus: true,
+		LogURI:    true,
+		LogMethod: true,
+		LogError:  true,
+		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+			if v.Error != nil {
+				log.Error().Msgf("Request: %s %s %d %s", v.Method, v.URI, v.Status, v.Error)
+			} else {
+				log.Info().Msgf("Request: %s %s %d", v.Method, v.URI, v.Status)
+			}
+			return nil
+		},
+	}))
 
 	if cfg.HasLegacyEIPFile() {
-		log.Info("Loading EIP file")
+		log.Info().Msg("Loading EIP file")
 	}
 
 	r, err := newRegistry(cfg)
 	if err != nil {
-		log.Fatal(err.Error())
+		log.Fatal().Msgf("Could not create registry: %s", err)
 	}
 
-	log.Infof("Starting load balancer on: %s", cfg.LoadBalancerAddr)
+	log.Info().Msgf("Starting load balancer on %s", cfg.LoadBalancerAddr)
 	lb, err := loadbalancer.StartLoadBalancer(cfg.LoadBalancerAddr)
 	if err != nil {
-		log.Fatal(err.Error())
+		log.Fatal().Msgf("Could not start load balancer: %s", err)
 	}
 	r.lb = lb
 
diff --git a/pkg/api/auth.go b/pkg/api/auth.go
index 9c783d376ee2a3de21ffc7ce0d36e424fbd5c917..672c593f26b3f57b91c78af215db5e2755d7d797 100644
--- a/pkg/api/auth.go
+++ b/pkg/api/auth.go
@@ -11,15 +11,15 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/apex/log"
 	"github.com/labstack/echo/v4"
+	"github.com/rs/zerolog/log"
 )
 
 func authTokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
 	return func(c echo.Context) error {
 		db, err := getDBFromContext(c)
 		if err != nil {
-			log.Errorf("Error getting database from context")
+			log.Warn().Msgf("Error getting database from context: %s", err)
 			return next(c)
 		}
 
@@ -34,7 +34,7 @@ func authTokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
 
 			var buckets string
 			if err := db.QueryRow("SELECT buckets FROM tokens WHERE key = ?", authTokenHashString).Scan(&buckets); err != nil {
-				log.Errorf("Error querying for access token: %v", err)
+				log.Warn().Msgf("Could not find invite toke in database: %v", err)
 			} else {
 				bucketsSlice := strings.Split(buckets, ",")
 				c.Set("buckets", bucketsSlice)
@@ -53,7 +53,7 @@ func agentRegistrationMiddleware(sharedSecret string) func(echo.HandlerFunc) ech
 			hmacHeaderBytes := make([]byte, hex.DecodedLen(len(hmacHeader)))
 			_, err := hex.Decode(hmacHeaderBytes, []byte(hmacHeader))
 			if err != nil {
-				log.Errorf("Error decoding hmac auth bytes: %v", err)
+				log.Warn().Msgf("Error decoding hmac auth bytes: %v", err)
 				return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")
 			}
 
@@ -67,7 +67,7 @@ func agentRegistrationMiddleware(sharedSecret string) func(echo.HandlerFunc) ech
 			calculatedHMAC := []byte(computed.Sum(nil))
 
 			if subtle.ConstantTimeCompare(hmacHeaderBytes, calculatedHMAC) == 0 {
-				log.Errorf("HMAC provided invalid. Provided: %v, expected: %v", hmacHeader, hex.EncodeToString(calculatedHMAC))
+				log.Warn().Msgf("HMAC provided invalid. Provided: %v, expected: %v", hmacHeader, hex.EncodeToString(calculatedHMAC))
 				return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")
 			}
 
diff --git a/pkg/api/cert.go b/pkg/api/cert.go
index 7bdacdbd866e706a4a00df03cb8a406dd24984ab..ecfbe84d3316b848331edd14c583cdc55af6c321 100644
--- a/pkg/api/cert.go
+++ b/pkg/api/cert.go
@@ -13,12 +13,13 @@ import (
 	"encoding/pem"
 	"fmt"
 	"io"
-	"log"
 	"math/big"
 	"net/http"
 	"strings"
 	"time"
 
+	"github.com/rs/zerolog/log"
+
 	"github.com/labstack/echo/v4"
 )
 
@@ -246,7 +247,7 @@ func writePEMFormattedString(addEnvelope bool, buf io.Writer, pemString, tag str
 func (r *registry) CertProxy(c echo.Context, addRootCA bool) error {
 	req, err := http.NewRequest(http.MethodGet, r.clientCertURL, nil)
 	if err != nil {
-		log.Printf("client: could not create request: %s\n", err)
+		log.Warn().Msgf("client: could not create request: %s", err)
 		return err
 	}
 	res, err := r.client.Do(req)
@@ -255,10 +256,9 @@ func (r *registry) CertProxy(c echo.Context, addRootCA bool) error {
 	}
 	resBody, err := io.ReadAll(res.Body)
 	if err != nil {
-		log.Printf("client: could not read response body: %s\n", err)
+		log.Warn().Msgf("client: could not read response body: %s", err)
 		return err
 	}
-	// log.Printf("client: response body: %s\n", resBody)
 	if !addRootCA {
 		return c.String(http.StatusOK, string(resBody))
 	}
diff --git a/pkg/api/gateway.go b/pkg/api/gateway.go
index a8bfca83ec56905a61d8e1a7d9cab7da2e962b91..7027777e3ba9bca3da5193e5a88c4ff6f2fa30ce 100644
--- a/pkg/api/gateway.go
+++ b/pkg/api/gateway.go
@@ -1,7 +1,6 @@
 package api
 
 import (
-	"log"
 	"math"
 	"math/rand"
 	"net/http"
@@ -10,6 +9,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/rs/zerolog/log"
+
 	"github.com/labstack/echo/v4"
 	"golang.org/x/exp/slices"
 
@@ -51,14 +52,16 @@ func (r *registry) GatewayPicker(c echo.Context) error {
 			keys = append(keys, k)
 		}
 		if !slices.Contains(keys, location) {
-			log.Println("[Debug]: specified location not in r.locations")
+			log.Debug().
+				Str("location", location).
+				Msg("specified location not in r.locations")
 			return c.JSON(http.StatusBadRequest, "Location not supported")
 		}
 		selectedLocation = location
-		log.Println("[Debug]: returning gateway for requested location", selectedLocation)
+		log.Debug().Msgf("returning gateway for requested location %s", selectedLocation)
 	} else if countryCode != "" {
 		// find nearest location for the given countryCode
-		log.Println("[Debug]: finding best gateway for Countrycode =", countryCode)
+		log.Debug().Msgf("finding best gateway for Countrycode = %s", countryCode)
 		clientCentroid, err := geolocate.GetCentroidForCountry(countryCode)
 		if err != nil {
 			return c.JSON(http.StatusBadRequest, "CountryCode not supported")
@@ -69,11 +72,11 @@ func (r *registry) GatewayPicker(c echo.Context) error {
 			gatewayLat, err1 := strconv.ParseFloat(loc.Lat, 64)
 			gatewayLon, err2 := strconv.ParseFloat(loc.Lon, 64)
 			if err1 != nil || err2 != nil {
-				log.Printf("invalid latitude or longitude for location: %s", loc.DisplayName)
+				log.Debug().Msgf("invalid latitude or longitude for location: %s", loc.DisplayName)
 			}
-			//log.Println(">>", loc.CountryCode, gatewayLat, gatewayLon)
+			log.Debug().Msgf(">> %s %f %f", loc.CountryCode, gatewayLat, gatewayLon)
 			distance := euclideanDistance(clientCentroid.Lat, clientCentroid.Lon, gatewayLat, gatewayLon)
-			log.Println("[Debug]: distance to", loc.Label, "::", distance)
+			log.Debug().Msgf("distance to %s :: %f", loc.Label, distance)
 
 			if distance < minDistance {
 				minDistance = distance
@@ -82,15 +85,15 @@ func (r *registry) GatewayPicker(c echo.Context) error {
 		}
 	} else {
 		// choose random location
-		log.Println("[Debug]: request without countrycode")
+		log.Debug().Msg("request without countrycode")
 		keys := make([]string, 0, len(r.locations))
 		for k := range r.locations {
 			keys = append(keys, k)
 		}
 
-		log.Println("[Debug]: returning gateway for random location")
+		log.Debug().Msg("returning gateway for random location")
 		selectedLocation = keys[rand.Intn(len(keys))]
-		log.Println("[Debug]: returning gateway for randomly chosen location", selectedLocation)
+		log.Debug().Msgf("returning gateway for randomly chosen location %s", selectedLocation)
 	}
 
 	gateways := r.gateways[selectedLocation]
@@ -163,7 +166,7 @@ func (r *registry) ListAllGateways(c echo.Context) error {
 		selectedLocationLabels = getLabelsForLocations(
 			geolocate.PickBestLocations(r.lm, cc, r.AllLocations()))
 	}
-	log.Printf("Selecting gateways from locations: %v", selectedLocationLabels)
+	log.Info().Msgf("Selecting gateways from locations: %v", selectedLocationLabels)
 
 	// Step 2a. We pass the pre-filter location selection to load balancer.
 	// If load filtering is not enabled, we just apply a filter for the selected locations.
@@ -174,7 +177,7 @@ func (r *registry) ListAllGateways(c echo.Context) error {
 		// We pass only a subset of the gateways to the geolocation picker
 		gatewaysByLoad, err := r.lb.SortGateways(selectedLocationLabels, gateways)
 		if err != nil {
-			log.Printf("Load lookup error: %v", err)
+			log.Warn().Msgf("Load lookup error: %v", err)
 			// we didn't succeed, so need to fallback to location-bucket selection.
 			// this usually means that the load balancer is not receiving updates
 			// from the agents matching the labels we've passed.
diff --git a/pkg/api/genconfig.go b/pkg/api/genconfig.go
index 4bc252bd97807bc2a0dc552833dd19b6f7c66605..d80bebeef8dfe514519f22f50ca7d1df3707d19b 100644
--- a/pkg/api/genconfig.go
+++ b/pkg/api/genconfig.go
@@ -3,10 +3,11 @@ package api
 import (
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"os"
 
+	"github.com/rs/zerolog/log"
+
 	"0xacab.org/leap/menshen/pkg/genconfig"
 	"github.com/labstack/echo/v4"
 )
@@ -93,19 +94,19 @@ func (r *registry) getOpenVPNConfig(cfg *Config) (string, error) {
 	if r.clientCertURL == "" {
 		rawCert, err = r.CertWriter(cfg.OvpnCaCrt, cfg.OvpnCaKey, cfg.Algo, cfg.OvpnClientCrtExpiry, true)
 		if err != nil {
-			log.Printf("getOpenvpnConfig: cert generation error: %s\n", err)
+			log.Warn().Msgf("getOpenvpnConfig: cert generation error: %v", err)
 			return rawCert, err
 		}
 
 		ca, err := os.ReadFile(cfg.OvpnCaCrt)
 		if err != nil {
-			log.Fatalf("getOpenvpnConfig: Could not read CA file '%s': %s", cfg.OvpnCaCrt, err)
+			log.Fatal().Msgf("getOpenvpnConfig: Could not read CA file '%s': %s", cfg.OvpnCaCrt, err)
 		}
 		CaCrt = string(ca)
 	} else {
 		req, err := http.NewRequest(http.MethodGet, r.clientCertURL, nil)
 		if err != nil {
-			log.Printf("genconfig get cert error: %s\n", err)
+			log.Warn().Msgf("genconfig get cert error: %s", err)
 			return "", err
 		}
 
@@ -123,7 +124,7 @@ func (r *registry) getOpenVPNConfig(cfg *Config) (string, error) {
 
 		resBody, err := io.ReadAll(res.Body)
 		if err != nil {
-			log.Printf("client: could not read response body: %s\n", err)
+			log.Warn().Msgf("client: could not read response body: %s", err)
 			return "", err
 
 		}
@@ -132,14 +133,14 @@ func (r *registry) getOpenVPNConfig(cfg *Config) (string, error) {
 
 		ca, err := os.ReadFile(cfg.CaFile)
 		if err != nil {
-			log.Fatalf("getOpenvpnConfig: Could not read CA file '%s': %s", cfg.CaFile, err)
+			log.Fatal().Msgf("getOpenvpnConfig: Could not read CA file '%s': %s", cfg.CaFile, err)
 		}
 		CaCrt = string(ca)
 	}
 
 	conf, err := genconfig.SerializeConfig(rawCert, CaCrt, gw)
 	if err != nil {
-		log.Printf("genconfig serialize error: %s\n", err)
+		log.Warn().Msgf("genconfig serialize error: %s", err)
 		return "", err
 	}
 	return conf, nil
diff --git a/pkg/api/registry.go b/pkg/api/registry.go
index a51036e3b4f004a478a45be74f0b029ec0ac97b0..932435be8f95d2b60ea1da106051e5a5946753b2 100644
--- a/pkg/api/registry.go
+++ b/pkg/api/registry.go
@@ -4,13 +4,14 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"os"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/rs/zerolog/log"
+
 	"0xacab.org/leap/menshen/pkg/geolocate"
 	"0xacab.org/leap/menshen/pkg/latency"
 	"0xacab.org/leap/menshen/pkg/loadbalancer"
@@ -63,7 +64,7 @@ func newRegistry(cfg *Config) (*registry, error) {
 	locations := make(locationMap, 0)
 
 	if cfg.ProviderJson == "" {
-		log.Fatal("File path to provider.json is missing.")
+		log.Fatal().Msg("File path to provider.json is missing.")
 	}
 	provider, err := ParseProviderJsonFile(cfg.ProviderJson)
 	if err != nil {
@@ -71,12 +72,12 @@ func newRegistry(cfg *Config) (*registry, error) {
 	}
 
 	if cfg.CaFile == "" {
-		log.Fatal("File path to root CA is missing.")
+		log.Fatal().Msg("File path to root CA is missing.")
 	}
 	var httpClient http.Client
 	ca, err := os.ReadFile(cfg.CaFile)
 	if err != nil {
-		log.Fatalf(fmt.Sprintf("Could not read CA file '%s': %s", cfg.CaFile, err))
+		log.Fatal().Msgf("Could not read CA file '%s': %s", cfg.CaFile, err)
 	}
 	httpClient = initTLSProxy(ca)
 
@@ -85,7 +86,7 @@ func newRegistry(cfg *Config) (*registry, error) {
 		// eip-service JSON format served as of version 3.
 		// TODO: this should be moved into its own parsing function.
 		if cfg.EIPURL != "" && cfg.EIP != "" {
-			log.Fatal("Both eip-url and eip-file cannot be set at the same time.")
+			log.Fatal().Msg("Both eip-url and eip-file cannot be set at the same time.")
 		}
 		var eipFile string
 
@@ -95,7 +96,7 @@ func newRegistry(cfg *Config) (*registry, error) {
 		} else {
 			// It must be that we have a URL, because HasLegacyEIPFile returns either or.
 			if err := downloadToFile(httpClient, cfg.EIPURL, tmpEIPFilePath); err != nil {
-				log.Fatalf("Could not fetch external EIP File: %s", err)
+				log.Fatal().Msgf("Could not fetch external EIP File: %s", err)
 			}
 			eipFile = tmpEIPFilePath
 			defer os.Remove(tmpEIPFilePath)
@@ -169,7 +170,7 @@ func newRegistry(cfg *Config) (*registry, error) {
 							if _, exists := locations[loc]; exists {
 								locations[loc].HasBridges = true
 							} else {
-								log.Fatalf("Could not find matching loation %s in locations list", loc)
+								log.Fatal().Msgf("Could not find matching loation %s in locations list", loc)
 							}
 						}
 					}
@@ -194,50 +195,50 @@ func newRegistry(cfg *Config) (*registry, error) {
 	// TODO move to a different map (for private bridges).
 
 	time.Sleep(time.Second) // just to make sure all nodes are up.
-	log.Printf("Bridges: %v", cfg.LocalBridges)
+	log.Info().Msgf("Bridges: %v", cfg.LocalBridges)
 
 	for _, controlBridge := range cfg.LocalBridges {
 		_parts := strings.Split(controlBridge, ":")
 		host := _parts[0]
 
 		url := fmt.Sprintf("http://%s/bridge", controlBridge)
-		log.Printf("Fetching bridge info from %s", url)
+		log.Info().Msgf("Fetching bridge info from %s", url)
 		resp, err := http.Get(url)
 		if err != nil {
-			log.Printf("error: %v", err)
+			log.Warn().Msgf("error: %v", err)
 			continue
 		}
 		defer resp.Body.Close()
 		if resp.StatusCode != http.StatusOK {
-			log.Printf("status code: %v", resp.StatusCode)
+			log.Warn().Msgf("status code: %v", resp.StatusCode)
 			continue
 		}
 		body, err := io.ReadAll(resp.Body)
 		if err != nil {
-			log.Printf("error reading: %v", err)
+			log.Warn().Msgf("error reading: %v", err)
 			continue
 		}
 		var bridge m.Bridge
 		if err := json.Unmarshal(body, &bridge); err != nil {
-			log.Printf("error unmarshal: %v", err)
+			log.Warn().Msgf("error unmarshal: %v", err)
 		}
 		bridge.Host = host
 		if bridge.Type == "obfs4" {
 			bridge.Transport = "tcp"
 		}
 		bridges[bridge.Location] = []*m.Bridge{&bridge}
-		log.Println("Parsed bridge status ok")
+		log.Info().Msg("Parsed bridge status ok")
 
 	}
 
-	log.Printf("%d locations", len(locations))
-	log.Printf("%d locations with gateways\n", len(gateways))
-	log.Printf("%d locations with bridges\n", len(bridges))
-	log.Println("openvpn_config:", openvpnConfig)
+	log.Info().Msgf("%d locations", len(locations))
+	log.Info().Msgf("%d locations with gateways", len(gateways))
+	log.Info().Msgf("%d locations with bridges", len(bridges))
+	log.Info().Msgf("openvpn_config: %s", openvpnConfig)
 
 	lm, err := latency.NewMetric()
 	if err != nil {
-		log.Println("Error initializing latency metric", err)
+		log.Warn().Msgf("Error initializing latency metric: %s", err)
 	}
 
 	r := &registry{
diff --git a/pkg/geolocate/distance.go b/pkg/geolocate/distance.go
index 1e9dce62fc292dff933b45b1b75f5d8259473ba8..fbcb1ccfe2d8207f7c8b8a7b09da5d44d97eed55 100644
--- a/pkg/geolocate/distance.go
+++ b/pkg/geolocate/distance.go
@@ -7,8 +7,8 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/apex/log"
 	g "github.com/kellydunn/golang-geo"
+	"github.com/rs/zerolog/log"
 	"golang.org/x/exp/slices"
 	"gonum.org/v1/gonum/floats"
 
@@ -43,12 +43,12 @@ func PickBestLocations(lm *latency.Metric, cc string, locations []*models.Locati
 	byLat = byLat[:min(len(byLat), maxLocations)]
 
 	for _, el := range byDist {
-		fmt.Println(">>> by distance", el.Label)
+		log.Info().Msgf(">>> by distance %s", el.Label)
 		set[el.Label] = el
 	}
 
 	for _, el := range byLat {
-		fmt.Println(">>> by latency", el.Label)
+		log.Info().Msgf(">>> by latency %s", el.Label)
 		set[el.Label] = el
 	}
 
@@ -204,9 +204,7 @@ func GetEstimatedLatencyBetweenCountries(lm *latency.Metric, from, to string) fl
 		return math.Inf(1)
 	}
 
-	//log.Debugf("nearest is: %s", nearest)
-	fmt.Printf("nearest is: %s\n", nearest)
-
+	log.Debug().Msgf("nearest is: %s", nearest)
 	// The long leg is a known latency metric (ms)
 	long := lm.Distance(to, nearest)
 
@@ -216,7 +214,7 @@ func GetEstimatedLatencyBetweenCountries(lm *latency.Metric, from, to string) fl
 		return math.Inf(1)
 	}
 
-	log.Debugf("short is: %.3f km apart", short)
+	log.Debug().Msgf("short is: %.3f km apart", short)
 
 	// ... so we just have to add a correction that takes the theoretical speed of light into account.
 	return long + getSpeedOfLightDelay(short)
diff --git a/pkg/geolocate/lookup.go b/pkg/geolocate/lookup.go
index 6296993c0c5512ad048cd37aec2b7a175346b83c..3b4fb2c7a553422a8a0754ec098361a2689c0a3f 100644
--- a/pkg/geolocate/lookup.go
+++ b/pkg/geolocate/lookup.go
@@ -3,6 +3,8 @@ package geolocate
 import (
 	"fmt"
 	"strings"
+
+	"github.com/rs/zerolog/log"
 )
 
 var ErrUnknownCountry = "unknown country code"
@@ -78,7 +80,7 @@ func ContinentCodeToLabel(i int) string {
 
 func GetContinentForCountryCode(cc string) int {
 	country := regions[strings.ToUpper(cc)]
-	fmt.Println("cc", country)
+	log.Debug().Msgf("cc %s", country)
 	if country == nil || country.Continent == "" {
 		return XX
 	}
diff --git a/pkg/latency/latency.go b/pkg/latency/latency.go
index 1619724a7e82286c45a493e12c9bcf44255ce537..50378655f9237d93c0cfaec923061a264f7bba73 100644
--- a/pkg/latency/latency.go
+++ b/pkg/latency/latency.go
@@ -3,10 +3,11 @@ package latency
 import (
 	"embed"
 	"encoding/csv"
-	"log"
 	"math"
 	"strconv"
 
+	"github.com/rs/zerolog/log"
+
 	"golang.org/x/exp/slices"
 	"gonum.org/v1/gonum/mat"
 )
@@ -23,7 +24,9 @@ func NewMetric() (*Metric, error) {
 	// Open the embedded latency source file
 	file, err := f.Open("distance.csv")
 	if err != nil {
-		log.Println("Error:", err)
+		log.Warn().
+			Err(err).
+			Msg("Could not open distance.csv in NewMetric")
 		return nil, err
 	}
 	defer file.Close()
@@ -34,7 +37,9 @@ func NewMetric() (*Metric, error) {
 	// Read all the records
 	records, err := reader.ReadAll()
 	if err != nil {
-		log.Println("Error:", err)
+		log.Warn().
+			Err(err).
+			Msg("Could not read from file")
 		return nil, err
 	}
 
diff --git a/pkg/loadbalancer/lb.go b/pkg/loadbalancer/lb.go
index c6283b2e3d56f11c0f1a13d49671b85574a7fa6e..f151b627ff9ec0a4461f968d10b94cec580ee157 100644
--- a/pkg/loadbalancer/lb.go
+++ b/pkg/loadbalancer/lb.go
@@ -25,7 +25,7 @@ import (
 	"0xacab.org/leap/menshen/pkg/models"
 	"git.autistici.org/ale/lb"
 	lbpb "git.autistici.org/ale/lb/proto"
-	"github.com/apex/log"
+	"github.com/rs/zerolog/log"
 	"google.golang.org/grpc"
 )
 
@@ -93,7 +93,9 @@ func StartLoadBalancer(bindAddr string) (*LoadBalancer, error) {
 	go func() {
 		err := server.Serve(listener)
 		if err != nil {
-			log.WithError(err).Error("grpc server Serve() failed")
+			log.Warn().
+				Err(err).
+				Msg("grpc server Serve() failed")
 		}
 	}()