134 lines
3.3 KiB
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
|
|
}
|
|
}
|