Files
go-omni/logx/redaction.go

134 lines
3.3 KiB
Go

package logx
import (
"log/slog"
"reflect"
"strings"
"time"
"unicode"
)
const redacted = "[REDACTED]"
// Redaction applies to structured keys, including nested maps and structs.
// Free text (messages, errors, URLs embedded in strings) is not parsed.
func redactor(extra []string) func([]string, slog.Attr) slog.Attr {
keys := make(map[string]bool)
for _, key := range append([]string{"password", "passwd", "secret", "clientSecret", "token", "accessToken", "refreshToken", "authorization", "cookie", "setCookie", "apiKey", "privateKey", "signedUrl"}, extra...) {
keys[normalizedKey(key)] = true
}
return func(groups []string, a slog.Attr) slog.Attr {
for _, group := range groups {
if keys[normalizedKey(group)] {
return slog.String(a.Key, redacted)
}
}
if keys[normalizedKey(a.Key)] {
return slog.String(a.Key, redacted)
}
a.Value = a.Value.Resolve()
if a.Value.Kind() == slog.KindAny {
a.Value = slog.AnyValue(sanitize(reflect.ValueOf(a.Value.Any()), keys, 0))
}
return a
}
}
func normalizedKey(key string) string {
return strings.Map(func(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return unicode.ToLower(r)
}
return -1
}, key)
}
func sanitize(v reflect.Value, keys map[string]bool, depth int) any {
if !v.IsValid() {
return nil
}
if depth >= 16 {
return "[TRUNCATED]"
}
if (v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface) && v.IsNil() {
return nil
}
if v.CanInterface() {
switch x := v.Interface().(type) {
case *slog.Source:
return sanitize(reflect.ValueOf(map[string]any{"function": x.Function, "file": x.File, "line": x.Line}), keys, depth+1)
case time.Time:
return x
case error:
return x.Error()
case []slog.Attr:
out := make(map[string]any, len(x))
for _, a := range x {
if keys[normalizedKey(a.Key)] {
out[a.Key] = redacted
} else {
out[a.Key] = sanitize(reflect.ValueOf(a.Value.Resolve().Any()), keys, depth+1)
}
}
return out
case slog.LogValuer:
value := slog.AnyValue(x).Resolve()
return sanitize(reflect.ValueOf(value.Any()), keys, depth+1)
}
}
switch v.Kind() {
case reflect.Interface, reflect.Pointer:
return sanitize(v.Elem(), keys, depth+1)
case reflect.Map:
if v.Type().Key().Kind() != reflect.String {
return "[UNSUPPORTED MAP]"
}
out := make(map[string]any, v.Len())
it := v.MapRange()
for it.Next() {
key := it.Key().String()
if keys[normalizedKey(key)] {
out[key] = redacted
} else {
out[key] = sanitize(it.Value(), keys, depth+1)
}
}
return out
case reflect.Struct:
out := make(map[string]any)
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
if !field.IsExported() {
continue
}
name := strings.Split(field.Tag.Get("json"), ",")[0]
if name == "-" {
continue
}
if name == "" {
name = field.Name
}
if keys[normalizedKey(name)] || keys[normalizedKey(field.Name)] {
out[name] = redacted
} else {
out[name] = sanitize(v.Field(i), keys, depth+1)
}
}
return out
case reflect.Slice, reflect.Array:
if v.Type().Elem().Kind() == reflect.Uint8 {
return "[BINARY]"
}
out := make([]any, v.Len())
for i := range out {
out[i] = sanitize(v.Index(i), keys, depth+1)
}
return out
default:
if v.CanInterface() {
return v.Interface()
}
return nil
}
}