Go cookbook · Beginner

Log structured JSON with slog and redact secrets

Logs are unstructured strings, and tokens or passwords end up in them.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Create a JSON slog handler with a level from configuration, attach common attributes with With, and implement slog.LogValuer on types that contain secrets so they're always redacted.

Solution

package main

import (
	"log/slog"
	"os"
)

type APIKey string

func (APIKey) LogValue() slog.Value { return slog.StringValue("[redacted]") }

type User struct {
	ID    int
	Email string
	Key   APIKey
}

func (u User) LogValue() slog.Value {
	return slog.GroupValue(slog.Int("id", u.ID), slog.String("email_domain", domain(u.Email)))
}

func domain(email string) string {
	for i := range email {
		if email[i] == '@' {
			return email[i+1:]
		}
	}
	return ""
}

func main() {
	level := new(slog.LevelVar)
	level.Set(slog.LevelInfo)
	if os.Getenv("LOG_LEVEL") == "debug" {
		level.Set(slog.LevelDebug)
	}
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
	logger = logger.With("service", "billing", "version", os.Getenv("APP_VERSION"))
	slog.SetDefault(logger)

	user := User{ID: 7, Email: "[email protected]", Key: "sk_live_123"}
	slog.Info("charge created", "user", user, "amount_cents", 2400, "key", user.Key)
	slog.Debug("only logged at debug level")
}

Pitfalls

  • Building messages with fmt.Sprintf instead of attributes, which makes logs hard to query.
  • Logging whole request bodies or structs with secrets.
  • Using different keys for the same thing (userId, user_id); agree on names.

Variations

  • Add trace and span IDs from OpenTelemetry with a custom handler.
  • Use slog.LevelVar to change the level at runtime (for example from an admin endpoint).

Further reading: log/slog

Reference: https://pkg.go.dev/log/slog

More Go recipes

Was this page helpful?