@@ -2,7 +2,9 @@ package ghostferry
22
33import (
44 "context"
5+ "fmt"
56 "log/slog"
7+ "reflect"
68)
79
810// loggerSlogHandler implements slog.Handler on top of a ghostferry Logger.
@@ -99,7 +101,98 @@ func applySlogAttrToLogger(l Logger, a slog.Attr, prefix string) Logger {
99101 if a .Key == "" {
100102 return l
101103 }
102- return l .WithField (joinSlogPrefix (prefix , a .Key ), a .Value .Any ())
104+ return l .WithField (joinSlogPrefix (prefix , a .Key ), safeFieldValue (a .Value .Any ()))
105+ }
106+
107+ // safeFieldValue returns a value safe to hand to a structured-logging backend.
108+ //
109+ // Some third-party libraries log values that cannot be JSON-encoded. For
110+ // example, go-mysql's BinlogSyncer logs its entire config via
111+ // slog.Any("config", cfg), and that config contains func fields
112+ // (Option func(*client.Conn) error, Dialer). logrus's JSON formatter calls
113+ // json.Marshal on each field and fails on such values, printing
114+ // "Failed to obtain reader, failed to marshal fields to JSON, json:
115+ // unsupported type: func(*client.Conn) error" to stderr for every binlog
116+ // syncer created — which floods the test logs.
117+ //
118+ // To stay backend-agnostic, any value that contains an unmarshalable kind
119+ // (func, chan, or unsafe.Pointer) is rendered to a string via fmt instead of
120+ // being passed through as a live Go value. Ordinary values are returned
121+ // unchanged so normal structured fields are unaffected.
122+ func safeFieldValue (v any ) any {
123+ if v == nil {
124+ return nil
125+ }
126+ if containsUnmarshalableKind (reflect .ValueOf (v ), 0 ) {
127+ return fmt .Sprintf ("%+v" , v )
128+ }
129+ return v
130+ }
131+
132+ // containsUnmarshalableKind reports whether v contains a func, chan, or
133+ // unsafe.Pointer anywhere in its (possibly nested) structure. It guards
134+ // against unbounded recursion with a depth limit and treats anything beyond it
135+ // as unmarshalable so the value is stringified rather than risk a marshal
136+ // failure.
137+ func containsUnmarshalableKind (v reflect.Value , depth int ) bool {
138+ if ! v .IsValid () {
139+ return false
140+ }
141+ if depth > 8 {
142+ return true
143+ }
144+
145+ switch v .Kind () {
146+ case reflect .Func , reflect .Chan , reflect .UnsafePointer :
147+ return true
148+ case reflect .Ptr , reflect .Interface :
149+ if v .IsNil () {
150+ return false
151+ }
152+ return containsUnmarshalableKind (v .Elem (), depth + 1 )
153+ case reflect .Struct :
154+ for i := 0 ; i < v .NumField (); i ++ {
155+ if containsUnmarshalableKind (v .Field (i ), depth + 1 ) {
156+ return true
157+ }
158+ }
159+ return false
160+ case reflect .Slice , reflect .Array :
161+ elem := v .Type ().Elem ()
162+ if isUnmarshalableType (elem ) {
163+ return true
164+ }
165+ for i := 0 ; i < v .Len (); i ++ {
166+ if containsUnmarshalableKind (v .Index (i ), depth + 1 ) {
167+ return true
168+ }
169+ }
170+ return false
171+ case reflect .Map :
172+ if isUnmarshalableType (v .Type ().Elem ()) || isUnmarshalableType (v .Type ().Key ()) {
173+ return true
174+ }
175+ for _ , k := range v .MapKeys () {
176+ if containsUnmarshalableKind (v .MapIndex (k ), depth + 1 ) {
177+ return true
178+ }
179+ }
180+ return false
181+ default :
182+ return false
183+ }
184+ }
185+
186+ // isUnmarshalableType reports whether a type is (or trivially contains) a kind
187+ // that cannot be JSON-encoded. Used to short-circuit empty containers whose
188+ // element type alone makes them unsafe.
189+ func isUnmarshalableType (t reflect.Type ) bool {
190+ switch t .Kind () {
191+ case reflect .Func , reflect .Chan , reflect .UnsafePointer :
192+ return true
193+ default :
194+ return false
195+ }
103196}
104197
105198// joinSlogPrefix concatenates prefix and key with a dot, eliding the dot when
0 commit comments