@@ -88,60 +88,160 @@ func formatValue(v interface{}) string {
8888
8989const tab = " "
9090
91- // Fmt returns a human readable format for ent.
91+ // bracketedLevel is an optimization to avoid extra allocations and calls to strings.ToLower
92+ // when we want to translate/print the lowercase version of a log level.
93+ func bracketedLevel (l slog.Level ) string {
94+ switch l {
95+ case slog .LevelDebug :
96+ return "[debu]"
97+ case slog .LevelInfo :
98+ return "[info]"
99+ case slog .LevelWarn :
100+ return "[warn]"
101+ case slog .LevelError :
102+ return "[erro]"
103+ case slog .LevelCritical :
104+ return "[crit]"
105+ case slog .LevelFatal :
106+ return "[fata]"
107+ default :
108+ return "[unkn]"
109+ }
110+ }
111+
112+ func writeSignedInt (w io.Writer , n int64 ) (bool , error ) {
113+ var a [20 ]byte
114+ _ , err := w .Write (strconv .AppendInt (a [:0 ], n , 10 ))
115+ return true , err
116+ }
117+
118+ func writeUnsignedInt (w io.Writer , n uint64 ) (bool , error ) {
119+ var a [20 ]byte
120+ _ , err := w .Write (strconv .AppendUint (a [:0 ], n , 10 ))
121+ return true , err
122+ }
123+
124+ // Optimization to avoid allocation of heap allocations/temporary strings via formatValue when dealing with primitive types.
125+ // It returns (handled, error). When handled is false, the caller should fall back to formatValue.
126+ func writeValueFast (w io.Writer , v interface {}) (bool , error ) {
127+ switch x := v .(type ) {
128+ case string :
129+ _ , err := w .Write ([]byte (quote (x )))
130+ return true , err
131+ case bool :
132+ if x {
133+ _ , err := w .Write ([]byte ("true" ))
134+ return true , err
135+ }
136+ _ , err := w .Write ([]byte ("false" ))
137+ return true , err
138+
139+ // signed ints
140+ case int :
141+ return writeSignedInt (w , int64 (x ))
142+ case int8 :
143+ return writeSignedInt (w , int64 (x ))
144+ case int16 :
145+ return writeSignedInt (w , int64 (x ))
146+ case int32 :
147+ return writeSignedInt (w , int64 (x ))
148+ case int64 :
149+ return writeSignedInt (w , x )
150+
151+ // unsigned ints
152+ case uint :
153+ return writeUnsignedInt (w , uint64 (x ))
154+ case uint8 :
155+ return writeUnsignedInt (w , uint64 (x ))
156+ case uint16 :
157+ return writeUnsignedInt (w , uint64 (x ))
158+ case uint32 :
159+ return writeUnsignedInt (w , uint64 (x ))
160+ case uint64 :
161+ return writeUnsignedInt (w , x )
162+
163+ // floats: prefer 'g' to keep output bounded (matches fmt default)
164+ case float32 :
165+ var a [32 ]byte
166+ _ , err := w .Write (strconv .AppendFloat (a [:0 ], float64 (x ), 'g' , - 1 , 32 ))
167+ return true , err
168+ case float64 :
169+ var a [32 ]byte
170+ _ , err := w .Write (strconv .AppendFloat (a [:0 ], x , 'g' , - 1 , 64 ))
171+ return true , err
172+ default :
173+ return false , nil
174+ }
175+ }
176+
177+ // Fmt returns a human readable format for ent. Assumes we have a bytes.Buffer
178+ // which we will more easily be able to assume underlying reallocation of it's size is possible
179+ // if necessary than for an arbitrary io.Writer/io.StringWriter
180+ // Note that while bytes.Buffer can in theory return an error for writes, it only does so if the buffer size will
181+ // exceed our architectures max integer size. If the system is actually OOM and more memory cannot be allocated
182+ // it will panic instead.
92183//
93184// We never return with a trailing newline because Go's testing framework adds one
94185// automatically and if we include one, then we'll get two newlines.
95186// We also do not indent the fields as go's test does that automatically
96187// for extra lines in a log so if we did it here, the fields would be indented
97188// twice in test logs. So the Stderr logger indents all the fields itself.
98- func Fmt (
99- buf interface {
100- io.StringWriter
101- io.Writer
102- }, termW io.Writer , ent slog.SinkEntry ,
103- ) {
189+ func Fmt (buf * bytes.Buffer , termW io.Writer , ent slog.SinkEntry ) {
104190 reset (buf , termW )
105- ts := ent .Time .Format (TimeFormat )
106- buf .WriteString (render (termW , timeStyle , ts + " " ))
107191
108- level := ent . Level . String ()
109- level = strings . ToLower ( level )
110- if len ( level ) > 4 {
111- level = level [: 4 ]
112- }
113- level = "[" + level + " ]"
114- buf .WriteString (render (termW , levelStyle (ent .Level ), level ))
192+ // Timestamp + space
193+ buf . WriteString ( render ( termW , timeStyle , ent . Time . Format ( TimeFormat )) )
194+ buf . WriteString ( " " )
195+
196+ // Level label + two spaces
197+ lvl := bracketedLevel ( ent . Level ) // e.g. "[debu]", "[info ]"
198+ buf .WriteString (render (termW , levelStyle (ent .Level ), lvl ))
115199 buf .WriteString (" " )
116200
201+ // Logger names: name1.name2.name3: (no strings.Join allocation)
117202 if len (ent .LoggerNames ) > 0 {
118- loggerName := quoteKey (strings .Join (ent .LoggerNames , "." )) + ": "
119- buf .WriteString (loggerName )
203+ for i , name := range ent .LoggerNames {
204+ if i > 0 {
205+ buf .WriteString ("." )
206+ }
207+ buf .WriteString (quoteKey (name ))
208+ }
209+ buf .WriteString (": " )
120210 }
121211
212+ // Message (detect multiline)
122213 var multilineKey string
123214 var multilineVal string
124215 msg := strings .TrimSpace (ent .Message )
125216 if strings .Contains (msg , "\n " ) {
126217 multilineKey = "msg"
127218 multilineVal = msg
128- msg = "..."
129- msg = quote (msg )
219+ msg = quote ("..." )
130220 }
131221 buf .WriteString (msg )
132222
223+ keyStyle := timeStyle
224+ equalsStyle := timeStyle
225+
226+ // Write trace/span directly (do not mutate ent.Fields)
133227 if ent .SpanContext .IsValid () {
134- ent .Fields = append (slog .M (
135- slog .F ("trace" , ent .SpanContext .TraceID ),
136- slog .F ("span" , ent .SpanContext .SpanID ),
137- ), ent .Fields ... )
228+ buf .WriteString (tab )
229+
230+ buf .WriteString (render (termW , keyStyle , quoteKey ("trace" )))
231+ buf .WriteString (render (termW , equalsStyle , "=" ))
232+ buf .WriteString (ent .SpanContext .TraceID ().String ())
233+ buf .WriteString (tab )
234+ buf .WriteString (render (termW , keyStyle , quoteKey ("span" )))
235+ buf .WriteString (render (termW , equalsStyle , "=" ))
236+ buf .WriteString (ent .SpanContext .SpanID ().String ())
138237 }
139238
239+ // Find a multiline field without mutating ent.Fields.
240+ multiIdx := - 1
140241 for i , f := range ent .Fields {
141242 if multilineVal != "" {
142243 break
143244 }
144-
145245 var s string
146246 switch v := f .Value .(type ) {
147247 case string :
@@ -153,46 +253,66 @@ func Fmt(
153253 if ! strings .Contains (s , "\n " ) {
154254 continue
155255 }
156-
157- // Remove this field.
158- ent .Fields = append (ent .Fields [:i ], ent .Fields [i + 1 :]... )
256+ multiIdx = i
159257 multilineKey = f .Name
160258 multilineVal = s
259+ break
161260 }
162261
163- keyStyle := timeStyle
164- // Help users distinguish logs by keeping some color in the equal signs.
165- equalsStyle := timeStyle
166-
262+ // Print fields (skip multiline field index).
167263 for i , f := range ent .Fields {
264+ if i == multiIdx {
265+ continue
266+ }
168267 if i < len (ent .Fields ) {
169268 buf .WriteString (tab )
170269 }
270+
171271 buf .WriteString (render (termW , keyStyle , quoteKey (f .Name )))
172272 buf .WriteString (render (termW , equalsStyle , "=" ))
173- valueStr := formatValue (f .Value )
174- buf .WriteString (valueStr )
273+
274+ if ok , err := writeValueFast (buf , f .Value ); err != nil {
275+ // return err
276+ } else if ! ok {
277+ buf .WriteString (formatValue (f .Value ))
278+ }
175279 }
176280
281+ // Multiline value block
177282 if multilineVal != "" {
178283 if msg != "..." {
179284 buf .WriteString (" ..." )
180285 }
181286
182- // Proper indentation.
183- lines := strings .Split (multilineVal , "\n " )
184- for i , line := range lines [1 :] {
185- if line != "" {
186- lines [i + 1 ] = strings .Repeat (" " , len (multilineKey )+ 2 ) + line
187- }
188- }
189- multilineVal = strings .Join (lines , "\n " )
190-
191- multilineKey = render (termW , keyStyle , multilineKey )
192287 buf .WriteString ("\n " )
193- buf .WriteString (multilineKey )
288+ buf .WriteString (render ( termW , keyStyle , multilineKey ) )
194289 buf .WriteString ("= " )
195- buf .WriteString (multilineVal )
290+
291+ // First line up to first newline
292+ s := multilineVal
293+ if n := strings .IndexByte (s , '\n' ); n >= 0 {
294+ buf .WriteString (s [:n ])
295+ s = s [n + 1 :]
296+ } else {
297+ buf .WriteString (s )
298+ s = ""
299+ }
300+
301+ indent := strings .Repeat (" " , len (multilineKey )+ 2 )
302+ for len (s ) > 0 {
303+ buf .WriteString ("\n " )
304+ // Only indent non-empty lines.
305+ if s [0 ] != '\n' {
306+ buf .WriteString (indent )
307+ }
308+ if n := strings .IndexByte (s , '\n' ); n >= 0 {
309+ buf .WriteString (s [:n ])
310+ s = s [n + 1 :]
311+ } else {
312+ buf .WriteString (s )
313+ break
314+ }
315+ }
196316 }
197317}
198318
0 commit comments