DEV Community

Masayoshi Mizutani
Masayoshi Mizutani

Posted on

Redacting Sensitive Data in Go's slog: A Practical Guide with masq

Logging is essential for debugging, monitoring, and auditing. But here's the catch—logs can accidentally expose sensitive data like passwords, API tokens, or personal information. Once sensitive data lands in your logs, removing it becomes a nightmare, especially when logs are immutable by design for compliance reasons.

In this post, I'll show you how to automatically redact sensitive values from your structured logs using Go's standard log/slog package and my open-source library masq.

The Problem: Sensitive Data Leaks in Logs

Consider a typical scenario: you're logging a user struct for debugging purposes.

type User struct {
    ID       string
    Email    string
    APIToken string
}

func main() {
    user := User{
        ID:       "u123",
        Email:    "alice@example.com",
        APIToken: "sk-secret-token-12345",
    }
    slog.Info("user logged in", "user", user)
}
Enter fullscreen mode Exit fullscreen mode

Output:

level=INFO msg="user logged in" user="{ID:u123 Email:alice@example.com APIToken:sk-secret-token-12345}"
Enter fullscreen mode Exit fullscreen mode

Oops. The API token is now in your logs, potentially accessible to anyone with log access, stored for months or years, and nearly impossible to delete.

slog's Built-in Solution: LogValuer

Go's slog package (part of the standard library since Go 1.21) provides LogValuer interface for customizing how values appear in logs:

type APIToken string

func (APIToken) LogValue() slog.Value {
    return slog.StringValue("[REDACTED]")
}

func main() {
    token := APIToken("sk-secret-token-12345")
    slog.Info("token received", "token", token)
}
Enter fullscreen mode Exit fullscreen mode

Output:

level=INFO msg="token received" token=[REDACTED]
Enter fullscreen mode Exit fullscreen mode

This works for direct values. However, LogValuer doesn't work for struct fields:

type APIToken string

func (APIToken) LogValue() slog.Value {
    return slog.StringValue("[REDACTED]")
}

type Credentials struct {
    UserID string
    Token  APIToken
}

func main() {
    creds := Credentials{
        UserID: "u123",
        Token:  "sk-secret-token-12345",
    }
    slog.Info("credentials", "creds", creds)
}
Enter fullscreen mode Exit fullscreen mode

Output (token is exposed!):

level=INFO msg=credentials creds="{UserID:u123 Token:sk-secret-token-12345}"
Enter fullscreen mode Exit fullscreen mode

When you log a struct, slog uses reflection and bypasses the LogValue() method on nested fields. This is a significant limitation for real-world applications.

Enter masq: Automatic Deep Redaction

I built masq to solve this problem. It hooks into slog's ReplaceAttr option and recursively inspects all logged values—including nested structs—to redact sensitive data.

Basic Usage

package main

import (
    "log/slog"
    "os"

    "github.com/m-mizutani/masq"
)

type EmailAddr string

type User struct {
    ID    string
    Email EmailAddr
}

func main() {
    // Create a logger with masq redaction
    logger := slog.New(
        slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithType[EmailAddr]()),
        }),
    )

    user := User{
        ID:    "u123",
        Email: "alice@example.com",
    }

    logger.Info("user registered", "user", user)
}
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "time": "2026-01-18T12:00:00.000Z",
  "level": "INFO",
  "msg": "user registered",
  "user": {
    "ID": "u123",
    "Email": "[FILTERED]"
  }
}
Enter fullscreen mode Exit fullscreen mode

The email is automatically redacted, even though it's nested inside the struct.

Redaction Strategies

masq offers multiple ways to identify sensitive data:

1. By Custom Type

Define sensitive data as distinct types and redact them:

type Password string
type CreditCard string

masq.New(
    masq.WithType[Password](),
    masq.WithType[CreditCard](),
)
Enter fullscreen mode Exit fullscreen mode

2. By Struct Tag

Mark sensitive fields with a struct tag:

type User struct {
    ID       string
    Password string `masq:"secret"`
    SSN      string `masq:"secret"`
}

masq.New(masq.WithTag("secret"))
Enter fullscreen mode Exit fullscreen mode

3. By Field Name

Target specific field names:

masq.New(
    masq.WithFieldName("Password"),
    masq.WithFieldName("APIKey"),
)
Enter fullscreen mode Exit fullscreen mode

4. By Field Prefix

Redact all fields starting with a prefix:

type Config struct {
    SecretKey      string  // redacted
    SecretToken    string  // redacted
    PublicEndpoint string  // not redacted
}

masq.New(masq.WithFieldPrefix("Secret"))
Enter fullscreen mode Exit fullscreen mode

5. By Regex Pattern

Match values against patterns (useful for credit cards, phone numbers, etc.):

import "regexp"

// Redact potential credit card numbers (16 digits)
cardPattern := regexp.MustCompile(`\b\d{16}\b`)

masq.New(masq.WithRegex(cardPattern))
Enter fullscreen mode Exit fullscreen mode

6. By String Content

Redact any value containing a specific string:

masq.New(masq.WithContain("Bearer "))
Enter fullscreen mode Exit fullscreen mode

Combining Multiple Strategies

You can combine multiple redaction rules:

logger := slog.New(
    slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        ReplaceAttr: masq.New(
            // Redact by type
            masq.WithType[Password](),
            masq.WithType[APIToken](),

            // Redact by struct tag
            masq.WithTag("secret"),

            // Redact fields with "Secret" prefix
            masq.WithFieldPrefix("Secret"),

            // Redact phone numbers
            masq.WithRegex(regexp.MustCompile(`^\+[1-9]\d{10,14}$`)),
        ),
    }),
)
Enter fullscreen mode Exit fullscreen mode

Real-World Example

Here's a complete example showing masq in a typical web application context:

package main

import (
    "log/slog"
    "os"
    "regexp"

    "github.com/m-mizutani/masq"
)

type (
    Password  string
    AuthToken string
)

type LoginRequest struct {
    Username string
    Password Password
}

type UserSession struct {
    UserID      string
    AuthToken   AuthToken
    Email       string `masq:"pii"`
    PhoneNumber string
}

func main() {
    // Configure comprehensive redaction
    logger := slog.New(
        slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            ReplaceAttr: masq.New(
                masq.WithType[Password](),
                masq.WithType[AuthToken](),
                masq.WithTag("pii"),
                masq.WithRegex(regexp.MustCompile(`^\+[1-9]\d{10,14}$`)), // phone numbers
            ),
        }),
    )
    slog.SetDefault(logger)

    // Simulate login flow
    req := LoginRequest{
        Username: "alice",
        Password: "super-secret-password",
    }
    slog.Info("login attempt", "request", req)

    session := UserSession{
        UserID:      "u123",
        AuthToken:   "tok_abc123xyz",
        Email:       "alice@example.com",
        PhoneNumber: "+14155551234",
    }
    slog.Info("session created", "session", session)
}
Enter fullscreen mode Exit fullscreen mode

Output:

{"time":"2026-01-18T12:00:00.000Z","level":"INFO","msg":"login attempt","request":{"Username":"alice","Password":"[FILTERED]"}}
{"time":"2026-01-18T12:00:00.000Z","level":"INFO","msg":"session created","session":{"UserID":"u123","AuthToken":"[FILTERED]","Email":"[FILTERED]","PhoneNumber":"[FILTERED]"}}
Enter fullscreen mode Exit fullscreen mode

All sensitive data is automatically redacted while preserving the structure and non-sensitive fields.

Best Practices

  1. Define custom types for sensitive data: Instead of using string for passwords or tokens, create distinct types like type Password string. This makes redaction explicit and catches issues at compile time.

  2. Use struct tags for external data: When working with third-party structs or database models, use the masq:"secret" tag to mark sensitive fields.

  3. Apply regex patterns carefully: Regex matching runs on every string value, so use specific patterns to avoid performance issues.

  4. Test your redaction: Write tests that verify sensitive data doesn't appear in log output.

  5. Default to redaction: When in doubt, redact. It's easier to remove redaction than to clean up leaked data.

Limitations

  • Private map fields: masq cannot reliably clone embedded private map types—they become nil. Use struct fields for sensitive data.
  • Performance: Deep inspection has some overhead. For high-throughput systems, consider sampling or async logging.

Conclusion

Preventing sensitive data leaks in logs is crucial for security and compliance. While slog's LogValuer helps with direct values, masq extends this protection to nested structs and provides flexible redaction strategies.

Give masq a try and let me know what you think!


If you found this helpful, feel free to star the repo on GitHub or share your feedback in the comments below.

Top comments (0)