Something went wrong. Try again.
An ATProto Powered Reputation Network
Something went wrong. Try again.
4.1 kB · 131 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132package logging
import ( "errors" "fmt" "io" "log/slog" "net/url" "os" "regexp" "strings")
const ( maxErrorDepth = 16 maxFieldBytes = 4096 redacted = "[redacted]")
var ( urlPattern = regexp.MustCompile(`https?://[^\s"'<>]+`) bearerPattern = regexp.MustCompile(`(?i)\bBearer\s+[^\s"',;]+`) secretPattern = regexp.MustCompile(`(?i)\b((?:access_token|refresh_token|accessJwt|refreshJwt|api_key|service_key|client_secret|encryption_key|password|authorization|cookie)["']?\s*[:=]\s*["']?)[^\s"',;}]+`))
func Setup(service string) error { level := slog.LevelInfo switch strings.ToLower(os.Getenv("PROVENANCE_LOG_LEVEL")) { case "", "info": case "debug": level = slog.LevelDebug case "warn": level = slog.LevelWarn case "error": level = slog.LevelError default: return errors.New("PROVENANCE_LOG_LEVEL must be debug, info, warn, or error") } secrets := []string{os.Getenv("PROVENANCE_SERVICE_KEY"), os.Getenv("PROVENANCE_ENCRYPTION_KEY"), os.Getenv("JETSTREAM_API_KEY")} if path := os.Getenv("JETSTREAM_API_KEY_FILE"); path != "" { if data, err := os.ReadFile(path); err == nil { secrets = append(secrets, strings.TrimSpace(string(data))) } } slog.SetDefault(New(os.Stderr, level, service, secrets)) return nil}
// Redact shared credentials and URL queries at the output boundary.func New(w io.Writer, level slog.Level, service string, secrets []string) *slog.Logger { secrets = append([]string(nil), secrets...) clean := func(value string) string { return sanitize(value, secrets) } handler := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level, ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { if sensitive(attr.Key) { return slog.String(attr.Key, redacted) } if err, ok := attr.Value.Any().(error); ok { attr.Value = Error(err) } if attr.Value.Kind() == slog.KindString { attr.Value = slog.StringValue(clean(attr.Value.String())) } return attr }}) return slog.New(handler).With("service", service)}
// Preserve causes hidden by wrappers such as domain.IndexError. Keep public error responses unchanged.func Error(err error) slog.Value { if err == nil { return slog.GroupValue() } var messages, types []string pending := []error{err} for depth := 0; len(pending) > 0 && depth < maxErrorDepth; depth++ { current := pending[len(pending)-1] pending = pending[:len(pending)-1] if current == nil { continue } message := current.Error() if !strings.Contains(strings.Join(messages, ": "), message) { messages = append(messages, message) } types = append(types, fmt.Sprintf("%T", current)) switch wrapped := current.(type) { case interface{ Unwrap() []error }: pending = append(pending, wrapped.Unwrap()...) case interface{ Unwrap() error }: pending = append(pending, wrapped.Unwrap()) } } return slog.GroupValue(slog.String("message", strings.Join(messages, ": ")), slog.Any("types", types))}
func sensitive(key string) bool { switch strings.ToLower(key) { case "authorization", "cookie", "set-cookie", "password", "secret", "api_key", "service_key", "encryption_key", "access_token", "refresh_token", "accessjwt", "refreshjwt", "client_secret": return true } return false}
func sanitize(value string, secrets []string) string { for _, secret := range secrets { if secret == "" { continue } for _, encoded := range []string{secret, url.QueryEscape(secret), url.PathEscape(secret)} { value = strings.ReplaceAll(value, encoded, redacted) } } value = bearerPattern.ReplaceAllString(value, "Bearer "+redacted) value = secretPattern.ReplaceAllString(value, "${1}"+redacted) value = urlPattern.ReplaceAllStringFunc(value, func(raw string) string { parsed, err := url.Parse(raw) if err != nil { if !strings.ContainsAny(raw, "?#@") { return raw } return "[redacted_url]" } parsed.User, parsed.RawQuery, parsed.Fragment = nil, "", "" parsed.ForceQuery = false return parsed.String() }) if len(value) > maxFieldBytes { value = value[:maxFieldBytes] + "..." } return value}