masq is a redacting utility to conceal sensitive data for slog that is official Go structured logging library. The concealing feature reduce risk to store secret values (API token, password and such things) and sensitive data like PII (Personal Identifiable Information) such as address, phone number, email address and etc into logging storage.
u := struct {
ID string
Email EmailAddr
}{
ID: "u123",
Email: "mizutani@hey.com",
}
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithType[EmailAddr]()),
},
),
)
logger.Info("hello", slog.Any("user", u))Then, output is following (jq formatted).
{
"time": "2022-12-25T09:00:00.123456789",
"level": "INFO",
"msg": "hello",
"user": {
"ID": "u123",
"Email": "[FILTERED]" // <- Concealed
}
}masq.New() provides a function for ReplaceAttr of slog.HandlerOptions. masq.New can specify one or multiple masq.Option to identify value and field to be concealed.
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(
// By user defined custom type
masq.WithType[AccessToken](),
// By regex of phone number as e164 format
masq.WithRegex(regexp.MustCompile(`^\+[1-9]\d{1,14}$`)),
// By field tag such as masq:"secret"
masq.WithTag("secret"),
// By by field name prefix. Concealing SecureXxx field
masq.WithFieldPrefix("Secure"),
),
},
),
)type password string
type myRecord struct {
ID string
Password password
}
record := myRecord{
ID: "m-mizutani",
Password: "abcd1234",
}
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithType[password]()),
},
),
)
logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Password":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}const issuedToken = "abcd1234"
authHeader := "Authorization: Bearer " + issuedToken
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithContain("abcd1234")),
},
),
)
logger.With("auth", authHeader).Info("send header")
out.Flush()
// Output:
// {"auth":"[REDACTED]","level":"INFO","msg":"send header","time":"2022-12-25T09:00:00.123456789"}type myRecord struct {
ID string
Phone string
}
record := myRecord{
ID: "m-mizutani",
Phone: "090-0000-0000",
}
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithRegex(regexp.MustCompile(`^\d{3}-\d{4}-\d{4}$`)),
},
),
)
logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}type myRecord struct {
ID string
EMail string `masq:"secret"`
}
record := myRecord{
ID: "m-mizutani",
EMail: "mizutani@hey.com",
}
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithTag("secret")),
},
),
)
logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"EMail":"[FILTERED]","ID":"m-mizutani"},"time":"2022-12-25T09:00:00.123456789"}You can change the tag key by masq.WithCustomTagKey option.
type myRecord struct {
ID string
EMail string `custom:"secret"`
}
record := myRecord{
ID: "m-mizutani",
EMail: "mizutani@hey.com",
}
logger := newLogger(out, masq.New(
masq.WithCustomTagKey("custom"),
masq.WithTag("secret"),
))
logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"EMail":"[REDACTED]","ID":"m-mizutani"},"time":"2022-12-25T09:00:00.123456789"}type myRecord struct {
ID string
Phone string
}
record := myRecord{
ID: "m-mizutani",
Phone: "090-0000-0000",
}
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithFieldName("Phone")),
},
),
)
logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","Phone":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}type myRecord struct {
ID string
SecurePhone string
}
record := myRecord{
ID: "m-mizutani",
SecurePhone: "090-0000-0000",
}
logger := slog.New(
slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithFieldPrefix("Secure")),
},
),
)
logger.With("record", record).Info("Got record")
out.Flush()
// Output:
// {"level":"INFO","msg":"Got record","record":{"ID":"m-mizutani","SecurePhone":"[FILTERED]"},"time":"2022-12-25T09:00:00.123456789"}Some data cannot be cloned and will become nil or zero values:
type privateMapType map[string]string
type Example struct {
// ❌ Cannot be cloned - these become nil/zero values
// Embedded private map types
privateMapType
}Result: These fields become nil or zero values in the output for security reasons. Note that:
- Maps with unexported key or value types can now be cloned, provided the types themselves are composed of fields that can be redacted
- Private interface fields (including
errortypes) are now properly preserved and can be redacted (fixed in v0.2.1+)
- Use struct fields instead of maps when storing sensitive data
- Keep maps in public fields with public key/value types
- Use struct tags (
masq:"secret") for reliable redaction
Apache License v2.0