Repository: google/exposure-notifications-server
Pull request #1255 (2020-12-28 22:26:58)

Improve control over logging package
The previous implementation conflated "development" mode with logging levels. Development mode is a special zap construct that changes the behavior of the logger whereas the log level is independent of the mode. This commit separates the mode from the level, allowing more control over the log levels.

It also codifies the environment variables used to find the level and mode in the logging package, which reduces some of the complexity of creating a logger.

**Release Note**

<!-- Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". -->

```release-note
Improve control over logging package
```

google-oss-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/1255#" title="Author self-approved">sethvargo</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/main/OWNERS)~~ [sethvargo]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
google-oss-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/1255#" title="Author self-approved">sethvargo</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/1255#pullrequestreview-559323743" title="Approved">whaught</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/main/OWNERS)~~ [sethvargo,whaught]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/cmd/cleanup-export/main.go b/cmd/cleanup-export/main.go
index e5cc064..1bb2e9f 100644
--- a/cmd/cleanup-export/main.go
+++ b/cmd/cleanup-export/main.go
@@ -19,8 +19,6 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/cleanup"
@@ -34,11 +32,9 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
-
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
 	ctx = logging.WithLogger(ctx, logger)
 
 	err := realMain(ctx)
diff --git a/cmd/cleanup-exposure/main.go b/cmd/cleanup-exposure/main.go
index 6af7dfd..9c496fb 100644
--- a/cmd/cleanup-exposure/main.go
+++ b/cmd/cleanup-exposure/main.go
@@ -19,8 +19,6 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/cleanup"
@@ -34,10 +32,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/debugger/main.go b/cmd/debugger/main.go
index 162ad09..5f4f2d4 100644
--- a/cmd/debugger/main.go
+++ b/cmd/debugger/main.go
@@ -18,8 +18,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/debugger"
@@ -32,10 +30,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/export-importer/main.go b/cmd/export-importer/main.go
index 7e59c8d..973fcc3 100644
--- a/cmd/export-importer/main.go
+++ b/cmd/export-importer/main.go
@@ -18,8 +18,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/exportimport"
@@ -32,10 +30,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/export/main.go b/cmd/export/main.go
index 6d71b44..9113295 100644
--- a/cmd/export/main.go
+++ b/cmd/export/main.go
@@ -18,8 +18,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/export"
@@ -33,10 +31,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/exposure/main.go b/cmd/exposure/main.go
index ab7b98c..3b4f8ee 100644
--- a/cmd/exposure/main.go
+++ b/cmd/exposure/main.go
@@ -20,7 +20,6 @@ import (
 	"fmt"
 	"net/http"
 	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/publish"
@@ -35,10 +34,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/federationin/main.go b/cmd/federationin/main.go
index 0bb09d0..8484de3 100644
--- a/cmd/federationin/main.go
+++ b/cmd/federationin/main.go
@@ -20,8 +20,6 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/federationin"
@@ -35,10 +33,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/federationout/main.go b/cmd/federationout/main.go
index 919698b..12cd605 100644
--- a/cmd/federationout/main.go
+++ b/cmd/federationout/main.go
@@ -18,8 +18,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
@@ -39,10 +37,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/generate/main.go b/cmd/generate/main.go
index 248373d..e7d8435 100644
--- a/cmd/generate/main.go
+++ b/cmd/generate/main.go
@@ -20,8 +20,6 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/generate"
@@ -35,10 +33,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/jwks-updater/main.go b/cmd/jwks-updater/main.go
index 8ae977a..68dc02f 100644
--- a/cmd/jwks-updater/main.go
+++ b/cmd/jwks-updater/main.go
@@ -19,8 +19,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/setup"
@@ -33,10 +31,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/key-rotation/main.go b/cmd/key-rotation/main.go
index 1588ac0..3184677 100644
--- a/cmd/key-rotation/main.go
+++ b/cmd/key-rotation/main.go
@@ -18,8 +18,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/keyrotation"
@@ -33,10 +31,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go
index ea0500d..4e34502 100644
--- a/cmd/migrate/main.go
+++ b/cmd/migrate/main.go
@@ -23,6 +23,7 @@ import (
 	"os"
 
 	"github.com/golang-migrate/migrate/v4"
+	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/setup"
 	"github.com/google/exposure-notifications-server/pkg/logging"
@@ -38,6 +39,11 @@ func main() {
 
 	ctx, done := signalcontext.OnInterrupt()
 
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
+
 	err := realMain(ctx)
 	done()
 
diff --git a/cmd/mirror/main.go b/cmd/mirror/main.go
index f6ebdcb..4db0698 100644
--- a/cmd/mirror/main.go
+++ b/cmd/mirror/main.go
@@ -18,8 +18,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/mirror"
@@ -32,10 +30,10 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
-	logger = logger.With("build_id", buildinfo.BuildID)
-	logger = logger.With("build_tag", buildinfo.BuildTag)
+	logger := logging.NewLoggerFromEnv().
+		With("build_id", buildinfo.BuildID).
+		With("build_tag", buildinfo.BuildTag)
+	ctx = logging.WithLogger(ctx, logger)
 
 	ctx = logging.WithLogger(ctx, logger)
 
diff --git a/docs/getting-started/development.md b/docs/getting-started/development.md
index 9d5d8fc..c0e3737 100644
--- a/docs/getting-started/development.md
+++ b/docs/getting-started/development.md
@@ -45,6 +45,10 @@ represent best practices.
     # Disable observability locally.
     export OBSERVABILITY_EXPORTER="NOOP"
 
+    # Configure local logging.
+    export LOG_LEVEL="debug"
+    export LOG_MODE="development"
+
     # Configure key management for revision tokens. Create your own revision
     # token AAD with:
     #
diff --git a/pkg/logging/logger.go b/pkg/logging/logger.go
index 6c36157..c1ff167 100644
--- a/pkg/logging/logger.go
+++ b/pkg/logging/logger.go
@@ -17,11 +17,11 @@ package logging
 
 import (
 	"context"
-	"fmt"
+	"os"
+	"strings"
 	"sync"
 	"time"
 
-	"go.opencensus.io/trace"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 )
@@ -40,20 +40,25 @@ var (
 )
 
 // NewLogger creates a new logger with the given configuration.
-func NewLogger(debug bool) *zap.SugaredLogger {
-	config := &zap.Config{
-		Level:            zap.NewAtomicLevelAt(zap.InfoLevel),
-		Development:      false,
-		Encoding:         encodingJSON,
-		EncoderConfig:    encoderConfig,
-		OutputPaths:      outputStderr,
-		ErrorOutputPaths: outputStderr,
-	}
-
-	// Add more details if logging is in debug mode.
-	if debug {
-		config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
-		config.Development = true
+func NewLogger(level string, development bool) *zap.SugaredLogger {
+	var config *zap.Config
+	if development {
+		config = &zap.Config{
+			Level:            zap.NewAtomicLevelAt(levelToZapLevel(level)),
+			Development:      true,
+			Encoding:         encodingConsole,
+			EncoderConfig:    developmentEncoderConfig,
+			OutputPaths:      outputStderr,
+			ErrorOutputPaths: outputStderr,
+		}
+	} else {
+		config = &zap.Config{
+			Level:            zap.NewAtomicLevelAt(levelToZapLevel(level)),
+			Encoding:         encodingJSON,
+			EncoderConfig:    productionEncoderConfig,
+			OutputPaths:      outputStderr,
+			ErrorOutputPaths: outputStderr,
+		}
 	}
 
 	logger, err := config.Build()
@@ -64,10 +69,19 @@ func NewLogger(debug bool) *zap.SugaredLogger {
 	return logger.Sugar()
 }
 
+// NewLoggerFromEnv creates a new logger from the environment. It consumes
+// LOG_LEVEL for determining the level and LOG_MODE for determining the output
+// parameters.
+func NewLoggerFromEnv() *zap.SugaredLogger {
+	level := os.Getenv("LOG_LEVEL")
+	development := strings.ToLower(strings.TrimSpace(os.Getenv("LOG_MODE"))) == "development"
+	return NewLogger(level, development)
+}
+
 // DefaultLogger returns the default logger for the package.
 func DefaultLogger() *zap.SugaredLogger {
 	defaultLoggerOnce.Do(func() {
-		defaultLogger = NewLogger(false)
+		defaultLogger = NewLoggerFromEnv()
 	})
 	return defaultLogger
 }
@@ -102,12 +116,13 @@ const (
 	levelAlert     = "ALERT"
 	levelEmergency = "EMERGENCY"
 
-	encodingJSON = "json"
+	encodingConsole = "console"
+	encodingJSON    = "json"
 )
 
 var outputStderr = []string{"stderr"}
 
-var encoderConfig = zapcore.EncoderConfig{
+var productionEncoderConfig = zapcore.EncoderConfig{
 	TimeKey:        timestamp,
 	LevelKey:       severity,
 	NameKey:        logger,
@@ -121,6 +136,44 @@ var encoderConfig = zapcore.EncoderConfig{
 	EncodeCaller:   zapcore.ShortCallerEncoder,
 }
 
+var developmentEncoderConfig = zapcore.EncoderConfig{
+	TimeKey:        "",
+	LevelKey:       "L",
+	NameKey:        "N",
+	CallerKey:      "C",
+	FunctionKey:    zapcore.OmitKey,
+	MessageKey:     "M",
+	StacktraceKey:  "S",
+	LineEnding:     zapcore.DefaultLineEnding,
+	EncodeLevel:    zapcore.CapitalLevelEncoder,
+	EncodeTime:     zapcore.ISO8601TimeEncoder,
+	EncodeDuration: zapcore.StringDurationEncoder,
+	EncodeCaller:   zapcore.ShortCallerEncoder,
+}
+
+// levelToZapLevel converts the given string to the appropriate zap level
+// value.
+func levelToZapLevel(s string) zapcore.Level {
+	switch strings.ToUpper(strings.TrimSpace(s)) {
+	case levelDebug:
+		return zapcore.DebugLevel
+	case levelInfo:
+		return zapcore.InfoLevel
+	case levelWarning:
+		return zapcore.WarnLevel
+	case levelError:
+		return zapcore.ErrorLevel
+	case levelCritical:
+		return zapcore.DPanicLevel
+	case levelAlert:
+		return zapcore.PanicLevel
+	case levelEmergency:
+		return zapcore.FatalLevel
+	}
+
+	return zapcore.WarnLevel
+}
+
 // levelEncoder transforms a zap level to the associated stackdriver level.
 func levelEncoder() zapcore.LevelEncoder {
 	return func(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
@@ -143,26 +196,6 @@ func levelEncoder() zapcore.LevelEncoder {
 	}
 }
 
-// TraceFromContext adds the correct Stackdriver trace fields.
-//
-// see: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
-func TraceFromContext(ctx context.Context) []zap.Field {
-	span := trace.FromContext(ctx)
-
-	if span == nil {
-		return nil
-	}
-
-	sc := span.SpanContext()
-
-	return []zap.Field{
-		// TODO(icco): Figure out how to add project ID to this.
-		zap.String("trace", fmt.Sprintf("traces/%s", sc.TraceID)),
-		zap.String("spanId", sc.SpanID.String()),
-		zap.Bool("traceSampled", sc.IsSampled()),
-	}
-}
-
 // timeEncoder encodes the time as RFC3339 nano
 func timeEncoder() zapcore.TimeEncoder {
 	return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
diff --git a/tools/seed/main.go b/tools/seed/main.go
index a48ef37..4d8ea0a 100644
--- a/tools/seed/main.go
+++ b/tools/seed/main.go
@@ -22,14 +22,13 @@ import (
 	"crypto/x509"
 	"encoding/pem"
 	"fmt"
-	"os"
 	"path/filepath"
 	"runtime"
-	"strconv"
 	"time"
 
 	authorizedappdatabase "github.com/google/exposure-notifications-server/internal/authorizedapp/database"
 	authorizedappmodel "github.com/google/exposure-notifications-server/internal/authorizedapp/model"
+	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/setup"
 	verificationdatabase "github.com/google/exposure-notifications-server/internal/verification/database"
@@ -43,8 +42,9 @@ import (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug)
+	logger := logging.NewLoggerFromEnv().Named("tools.seed")
+	logger = logger.With("build_id", buildinfo.BuildID)
+	logger = logger.With("build_tag", buildinfo.BuildTag)
 	ctx = logging.WithLogger(ctx, logger)
 
 	err := realMain(ctx)
diff --git a/tools/sign/main.go b/tools/sign/main.go
index 9ef1e5a..24b4900 100644
--- a/tools/sign/main.go
+++ b/tools/sign/main.go
@@ -24,8 +24,6 @@ import (
 	"encoding/base64"
 	"flag"
 	"fmt"
-	"os"
-	"strconv"
 	"time"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
@@ -43,11 +41,9 @@ var (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug).Named("tools.sign")
+	logger := logging.NewLoggerFromEnv().Named("tools.sign")
 	logger = logger.With("build_id", buildinfo.BuildID)
 	logger = logger.With("build_tag", buildinfo.BuildTag)
-
 	ctx = logging.WithLogger(ctx, logger)
 
 	err := realMain(ctx)
diff --git a/tools/verify/main.go b/tools/verify/main.go
index 87099e9..8d24439 100644
--- a/tools/verify/main.go
+++ b/tools/verify/main.go
@@ -26,8 +26,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"math/big"
-	"os"
-	"strconv"
 
 	"github.com/google/exposure-notifications-server/internal/buildinfo"
 	"github.com/google/exposure-notifications-server/pkg/base64util"
@@ -45,11 +43,9 @@ var (
 func main() {
 	ctx, done := signalcontext.OnInterrupt()
 
-	debug, _ := strconv.ParseBool(os.Getenv("LOG_DEBUG"))
-	logger := logging.NewLogger(debug).Named("tools.verify")
+	logger := logging.NewLoggerFromEnv().Named("tools.verify")
 	logger = logger.With("build_id", buildinfo.BuildID)
 	logger = logger.With("build_tag", buildinfo.BuildTag)
-
 	ctx = logging.WithLogger(ctx, logger)
 
 	err := realMain(ctx)


Test patch:
diff --git a/internal/publish/publish_test.go b/internal/publish/publish_test.go
index 5ea1b92..10d91f8 100644
--- a/internal/publish/publish_test.go
+++ b/internal/publish/publish_test.go
@@ -42,7 +42,6 @@ import (
 	vermodel "github.com/google/exposure-notifications-server/internal/verification/model"
 	"github.com/google/exposure-notifications-server/pkg/base64util"
 	"github.com/google/exposure-notifications-server/pkg/keys"
-	"github.com/google/exposure-notifications-server/pkg/logging"
 	"github.com/google/exposure-notifications-server/pkg/timeutils"
 	"github.com/google/exposure-notifications-server/pkg/util"
 	"github.com/jackc/pgx/v4"
@@ -485,10 +484,6 @@ func TestPublishWithBypass(t *testing.T) {
 			}
 
 			t.Run(addVer+tc.Name, func(t *testing.T) {
-				ctx = context.Background()
-				logger := logging.NewLogger(true)
-				ctx = logging.WithLogger(ctx, logger)
-
 				// And set up publish handler up front.
 				config := Config{}
 				config.AuthorizedApp.CacheDuration = time.Nanosecond
diff --git a/pkg/logging/logger_test.go b/pkg/logging/logger_test.go
index 7a13fd6..6eaa09e 100644
--- a/pkg/logging/logger_test.go
+++ b/pkg/logging/logger_test.go
@@ -24,7 +24,7 @@ import (
 func TestNewLogger(t *testing.T) {
 	t.Parallel()
 
-	logger := logging.NewLogger(true)
+	logger := logging.NewLogger("", true)
 	if logger == nil {
 		t.Fatal("expected logger to never be nil")
 	}
diff --git a/terraform-e2e-ci/main.tf b/terraform-e2e-ci/main.tf
index 8beaa1f..dbc587e 100644
--- a/terraform-e2e-ci/main.tf
+++ b/terraform-e2e-ci/main.tf
@@ -43,17 +43,18 @@ module "en" {
       TRUNCATE_WINDOW = "1s"
       MIN_WINDOW_AGE  = "1s"
 
-      LOG_DEBUG = "true"
+      LOG_LEVEL = "debug"
     }
 
     exposure = {
       TRUNCATE_WINDOW             = "1s"
       DEBUG_RELEASE_SAME_DAY_KEYS = true
-      LOG_DEBUG                   = "true"
+
+      LOG_LEVEL = "debug"
     }
 
     generate = {
-      LOG_DEBUG = "true"
+      LOG_LEVEL = "debug"
     }
   }
 }