
Logging is essential for debugging, monitoring, and auditing. But here's the catch—logs can...
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.
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.
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)
}
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 is a significant limitation for real-world applications.
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.
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)
}
Output:
{
"time": "2026-01-18T12:00:00.000Z",
"level": "INFO",
"msg": "user registered",
"user": {
"ID": "u123",
"Email": "[FILTERED]"
}
}
The email is automatically redacted, even though it's nested inside the struct.
masq offers multiple ways to identify sensitive data:
Define sensitive data as distinct types and redact them:
type Password string
type CreditCard string
masq.New(
masq.WithType[Password](),
masq.WithType[CreditCard](),
)
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"))
Target specific field names:
masq.New(
masq.WithFieldName("Password"),
masq.WithFieldName("APIKey"),
)
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"))
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))
Redact any value containing a specific string:
masq.New(masq.WithContain("Bearer "))
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}$`)),
),
}),
)
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)
}
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.
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.
Use struct tags for external data: When working with third-party structs or database models, use the 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.
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.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...
Workflows from the Neura Market marketplace related to this DeepSeek resource