package slogcommon import ( "encoding" "fmt" "log/slog" "net/http" "reflect" "runtime" "slices" "strings" "github.com/samber/lo" ) type ReplaceAttrFn = func(groups []string, a slog.Attr) slog.Attr func AppendRecordAttrsToAttrs(attrs []slog.Attr, groups []string, record *slog.Record) []slog.Attr { output := make([]slog.Attr, 0, len(attrs)+record.NumAttrs()) output = append(output, attrs...) record.Attrs(func(attr slog.Attr) bool { for i := len(groups) - 1; i >= 0; i-- { attr = slog.Group(groups[i], attr) } output = append(output, attr) return true }) return output } func ReplaceAttrs(fn ReplaceAttrFn, groups []string, attrs ...slog.Attr) []slog.Attr { for i := range attrs { attr := attrs[i] value := attr.Value.Resolve() if value.Kind() == slog.KindGroup { attrs[i].Value = slog.GroupValue(ReplaceAttrs(fn, append(groups, attr.Key), value.Group()...)...) } else if fn != nil { attrs[i] = fn(groups, attr) } else { attrs[i].Value = value } } return attrs } func AttrsToMap(attrs ...slog.Attr) map[string]any { output := map[string]any{} attrsByKey := groupValuesByKey(attrs) for k, values := range attrsByKey { v := mergeAttrValues(values...) if v.Kind() == slog.KindGroup { output[k] = AttrsToMap(v.Group()...) } else { output[k] = v.Any() } } return output } func RecordToAttrsMap(r slog.Record) map[string]any { attrs := make([]slog.Attr, 0, r.NumAttrs()) r.Attrs(func(attr slog.Attr) bool { attrs = append(attrs, attr) return true }) return AttrsToMap(attrs...) } func groupValuesByKey(attrs []slog.Attr) map[string][]slog.Value { result := map[string][]slog.Value{} for _, item := range attrs { key := item.Key result[key] = append(result[key], item.Value) } return result } func mergeAttrValues(values ...slog.Value) slog.Value { v := values[0] for i := 1; i < len(values); i++ { if v.Kind() != slog.KindGroup || values[i].Kind() != slog.KindGroup { v = values[i] continue } v = slog.GroupValue(append(v.Group(), values[i].Group()...)...) } return v } func AttrToValue(attr slog.Attr) (string, any) { k := attr.Key v := attr.Value kind := v.Kind() switch kind { case slog.KindAny: return k, v.Any() case slog.KindLogValuer: return k, v.Any() case slog.KindGroup: return k, AttrsToMap(v.Group()...) case slog.KindInt64: return k, v.Int64() case slog.KindUint64: return k, v.Uint64() case slog.KindFloat64: return k, v.Float64() case slog.KindString: return k, v.String() case slog.KindBool: return k, v.Bool() case slog.KindDuration: return k, v.Duration() case slog.KindTime: return k, v.Time().UTC() default: return k, AnyValueToString(v) } } func AnyValueToString(v slog.Value) string { if tm, ok := v.Any().(encoding.TextMarshaler); ok { data, err := tm.MarshalText() if err != nil { return "" } return string(data) } return fmt.Sprintf("%+v", v.Any()) } func AttrsToString(attrs ...slog.Attr) map[string]string { output := make(map[string]string, len(attrs)) for i := range attrs { attr := attrs[i] k, v := attr.Key, attr.Value output[k] = ValueToString(v) } return output } func ValueToString(v slog.Value) string { switch v.Kind() { case slog.KindAny, slog.KindLogValuer, slog.KindGroup: return AnyValueToString(v) case slog.KindInt64, slog.KindUint64, slog.KindFloat64, slog.KindString, slog.KindBool, slog.KindDuration: return v.String() case slog.KindTime: return v.Time().UTC().String() default: return AnyValueToString(v) } } func ReplaceError(attrs []slog.Attr, errorKeys ...string) []slog.Attr { replaceAttr := func(groups []string, a slog.Attr) slog.Attr { if len(groups) > 1 { return a } for i := range errorKeys { if a.Key == errorKeys[i] { if err, ok := a.Value.Any().(error); ok { return slog.Any(a.Key, FormatError(err)) } } } return a } return ReplaceAttrs(replaceAttr, []string{}, attrs...) } func ExtractError(attrs []slog.Attr, errorKeys ...string) ([]slog.Attr, error) { for i := range attrs { attr := attrs[i] if !slices.Contains(errorKeys, attr.Key) { continue } if err, ok := attr.Value.Resolve().Any().(error); ok { output := make([]slog.Attr, 0, len(attrs)-1) output = append(output, attrs[:i]...) output = append(output, attrs[i+1:]...) return output, err } } return attrs, nil } func FormatErrorKey(values map[string]any, errorKeys ...string) map[string]any { for _, errorKey := range errorKeys { if err, ok := values[errorKey]; ok { if e, ok := err.(error); ok { values[errorKey] = FormatError(e) break } } } return values } func FormatError(err error) any { if e, ok := err.(slog.LogValuer); ok { return e.LogValue() } return map[string]any{ "kind": reflect.TypeOf(err).String(), "error": err.Error(), "stack": nil, // @TODO } } func FormatRequest(req *http.Request, ignoreHeaders bool) map[string]any { output := map[string]any{ "host": req.Host, "method": req.Method, "url": map[string]any{ "url": req.URL.String(), "scheme": req.URL.Scheme, "host": req.URL.Host, "path": req.URL.Path, "raw_query": req.URL.RawQuery, "fragment": req.URL.Fragment, "query": lo.MapEntries(req.URL.Query(), func(key string, values []string) (string, string) { return key, strings.Join(values, ",") }), }, } if !ignoreHeaders { output["headers"] = lo.MapEntries(req.Header, func(key string, values []string) (string, string) { return key, strings.Join(values, ",") }) } return output } func Source(sourceKey string, r *slog.Record) slog.Attr { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() var args []any if f.Function != "" { args = append(args, slog.String("function", f.Function)) } if f.File != "" { args = append(args, slog.String("file", f.File)) } if f.Line != 0 { args = append(args, slog.Int("line", f.Line)) } return slog.Group(sourceKey, args...) } func StringSource(sourceKey string, r *slog.Record) slog.Attr { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() return slog.String(sourceKey, fmt.Sprintf("%s:%d (%s)", f.File, f.Line, f.Function)) } func FindAttribute(attrs []slog.Attr, groups []string, key string) (slog.Attr, bool) { // group traversal if len(groups) > 0 { for _, attr := range attrs { if attr.Value.Kind() == slog.KindGroup && attr.Key == groups[0] { attr, found := FindAttribute(attr.Value.Group(), groups[1:], key) if found { return attr, true } } } return slog.Attr{}, false } // starting here, groups is empty for _, attr := range attrs { if attr.Key == key { return attr, true } } return slog.Attr{}, false } func RemoveEmptyAttrs(attrs []slog.Attr) []slog.Attr { return lo.FlatMap(attrs, func(attr slog.Attr, _ int) []slog.Attr { if attr.Key != "" { if attr.Value.Kind() == slog.KindGroup { values := RemoveEmptyAttrs(attr.Value.Group()) if len(values) == 0 { return nil } attr.Value = slog.GroupValue(values...) } if attr.Value.Equal(slog.Value{}) { return nil } return []slog.Attr{attr} } if attr.Value.Kind() != slog.KindGroup { return nil } return RemoveEmptyAttrs(attr.Value.Group()) }) }