Redacting Sensitive Data in Go's slog: A Practical Guide with masq
Source: Dev.to
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)
}
Output
level=INFO msg="user logged in" user="{ID:u123 Email:alice@example.com APIToken:sk-secret-token-12345}"
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 (standard library since Go 1.21) provides the 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)
}
Output
level=INFO msg="token received" token=[REDACTED]
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)
}
Output (token is exposed!)
level=INFO msg=credentials creds="{UserID:u123 Token:sk-secret-token-12345}"
When you log a struct, slog uses reflection and bypasses the LogValue() method on nested fields. This limitation makes it hard to protect sensitive data in real‑world applications.
masq: Automatic Deep Redaction
masq 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)
}
Example Output
{
"time": "2026-01-18T12:00:00.000Z",
"level": "INFO",
"msg": "user registered",
"user": {
"ID": "u123",
"Email": "[FILTERED]"
}
}
The email address is automatically redacted, even though it resides inside a nested struct.
Redaction Strategies
masq provides several ways to identify and redact 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](),
)
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"))
3. By Field Name
Target specific field names:
masq.New(
masq.WithFieldName("Password"),
masq.WithFieldName("APIKey"),
)
4. By Field Prefix
Redact all fields that start with a given prefix:
type Config struct {
SecretKey string // redacted
SecretToken string // redacted
PublicEndpoint string // not redacted
}
masq.New(masq.WithFieldPrefix("Secret"))
5. By Regex Pattern
Match values against a regular expression (useful for credit‑card numbers, phone numbers, etc.):
import "regexp"
// Redact potential credit‑card numbers (16 digits)
cardPattern := regexp.MustCompile(`\b\d{16}\b`)
masq.New(masq.WithRegex(cardPattern))
6. By String Content
Redact any value that contains a specific substring:
masq.New(masq.WithContain("Bearer "))
Combining Multiple Strategies
You can combine any number of 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 tag
masq.WithTag("secret"),
// Redact by field name
masq.WithFieldName("SSN"),
// Redact by prefix
masq.WithFieldPrefix("Secret"),
// Redact by regex
masq.WithRegex(regexp.MustCompile(`\b\d{16}\b`)),
// Redact by string content
masq.WithContain("Bearer "),
),
}),
)
Now every log entry passes through all configured filters, ensuring that no sensitive information slips through—no matter how deeply it’s nested.
TL;DR
- Problem:
slog’sLogValuercan’t redact nested fields. - Solution: Use masq to recursively inspect and filter logged values.
- How: Plug
masq.New(...)intoslog.HandlerOptions.ReplaceAttrand choose one or more identification strategies (type, tag, field name, prefix, regex, or string content).
Give it a try, and keep your logs clean and compliant!
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 `masq:"pii"`
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)
}
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]"}}
All sensitive data is automatically redacted while preserving the structure and non‑sensitive fields.
Best Practices
- Define custom types for sensitive data – Instead of using a plain
stringfor passwords or tokens, create distinct types (e.g.,type Password string). This makes redaction explicit and catches issues at compile time. - Use struct tags for external data – When working with third‑party structs or database models, add a
masq:"secret"tag to mark sensitive fields. - Apply regex patterns carefully – Regex matching runs on every string value, so use specific patterns to avoid performance issues.
- Test your redaction – Write tests that verify sensitive data doesn’t appear in log output.
- Default to redaction – When in doubt, redact. It’s easier to remove redaction than to clean up leaked data.
Limitations
-
Private map fields –
masqcannot reliably clone embedded private map types; they becomenil.
Use struct fields for sensitive data instead. -
Performance – Deep inspection adds some overhead.
For high‑throughput systems, consider sampling or asynchronous 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 us know what you think!
If you found this helpful, feel free to ⭐ the repo on GitHub or share your feedback in the comments below.