text.go

  1package log
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strings"
  7	"sync"
  8	"time"
  9	"unicode"
 10	"unicode/utf8"
 11)
 12
 13const (
 14	separator       = "="
 15	indentSeparator = "  │ "
 16)
 17
 18func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline bool, key string) {
 19	st := l.styles
 20
 21	// kindly borrowed from hclog
 22	for {
 23		nl := strings.IndexByte(str, '\n')
 24		if nl == -1 {
 25			if str != "" {
 26				_, _ = w.Write([]byte(indent))
 27				val := escapeStringForOutput(str, false)
 28				if valueStyle, ok := st.Values[key]; ok {
 29					val = valueStyle.Render(val)
 30				} else {
 31					val = st.Value.Render(val)
 32				}
 33				_, _ = w.Write([]byte(val))
 34				if newline {
 35					_, _ = w.Write([]byte{'\n'})
 36				}
 37			}
 38			return
 39		}
 40
 41		_, _ = w.Write([]byte(indent))
 42		val := escapeStringForOutput(str[:nl], false)
 43		val = st.Value.Render(val)
 44		_, _ = w.Write([]byte(val))
 45		_, _ = w.Write([]byte{'\n'})
 46		str = str[nl+1:]
 47	}
 48}
 49
 50func needsEscaping(str string) bool {
 51	for _, b := range str {
 52		if !unicode.IsPrint(b) || b == '"' {
 53			return true
 54		}
 55	}
 56
 57	return false
 58}
 59
 60const (
 61	lowerhex = "0123456789abcdef"
 62)
 63
 64var bufPool = sync.Pool{
 65	New: func() interface{} {
 66		return new(strings.Builder)
 67	},
 68}
 69
 70func escapeStringForOutput(str string, escapeQuotes bool) string {
 71	// kindly borrowed from hclog
 72	if !needsEscaping(str) {
 73		return str
 74	}
 75
 76	bb := bufPool.Get().(*strings.Builder)
 77	bb.Reset()
 78
 79	defer bufPool.Put(bb)
 80	for _, r := range str {
 81		if escapeQuotes && r == '"' {
 82			bb.WriteString(`\"`)
 83		} else if unicode.IsPrint(r) {
 84			bb.WriteRune(r)
 85		} else {
 86			switch r {
 87			case '\a':
 88				bb.WriteString(`\a`)
 89			case '\b':
 90				bb.WriteString(`\b`)
 91			case '\f':
 92				bb.WriteString(`\f`)
 93			case '\n':
 94				bb.WriteString(`\n`)
 95			case '\r':
 96				bb.WriteString(`\r`)
 97			case '\t':
 98				bb.WriteString(`\t`)
 99			case '\v':
100				bb.WriteString(`\v`)
101			default:
102				switch {
103				case r < ' ':
104					bb.WriteString(`\x`)
105					bb.WriteByte(lowerhex[byte(r)>>4])
106					bb.WriteByte(lowerhex[byte(r)&0xF])
107				case !utf8.ValidRune(r):
108					r = 0xFFFD
109					fallthrough
110				case r < 0x10000:
111					bb.WriteString(`\u`)
112					for s := 12; s >= 0; s -= 4 {
113						bb.WriteByte(lowerhex[r>>uint(s)&0xF])
114					}
115				default:
116					bb.WriteString(`\U`)
117					for s := 28; s >= 0; s -= 4 {
118						bb.WriteByte(lowerhex[r>>uint(s)&0xF])
119					}
120				}
121			}
122		}
123	}
124
125	return bb.String()
126}
127
128func needsQuoting(s string) bool {
129	for i := 0; i < len(s); {
130		b := s[i]
131		if b < utf8.RuneSelf {
132			if needsQuotingSet[b] {
133				return true
134			}
135			i++
136			continue
137		}
138		r, size := utf8.DecodeRuneInString(s[i:])
139		if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) {
140			return true
141		}
142		i += size
143	}
144	return false
145}
146
147var needsQuotingSet = [utf8.RuneSelf]bool{
148	'"': true,
149	'=': true,
150}
151
152func init() {
153	for i := 0; i < utf8.RuneSelf; i++ {
154		r := rune(i)
155		if unicode.IsSpace(r) || !unicode.IsPrint(r) {
156			needsQuotingSet[i] = true
157		}
158	}
159}
160
161func writeSpace(w io.Writer, first bool) {
162	if !first {
163		w.Write([]byte{' '}) //nolint: errcheck
164	}
165}
166
167func (l *Logger) textFormatter(keyvals ...interface{}) {
168	st := l.styles
169	lenKeyvals := len(keyvals)
170
171	for i := 0; i < lenKeyvals; i += 2 {
172		firstKey := i == 0
173		moreKeys := i < lenKeyvals-2
174
175		switch keyvals[i] {
176		case TimestampKey:
177			if t, ok := keyvals[i+1].(time.Time); ok {
178				ts := t.Format(l.timeFormat)
179				ts = st.Timestamp.Render(ts)
180				writeSpace(&l.b, firstKey)
181				l.b.WriteString(ts)
182			}
183		case LevelKey:
184			if level, ok := keyvals[i+1].(Level); ok {
185				var lvl string
186				lvlStyle, ok := st.Levels[level]
187				if !ok {
188					continue
189				}
190
191				lvl = lvlStyle.String()
192				if lvl != "" {
193					writeSpace(&l.b, firstKey)
194					l.b.WriteString(lvl)
195				}
196			}
197		case CallerKey:
198			if caller, ok := keyvals[i+1].(string); ok {
199				caller = fmt.Sprintf("<%s>", caller)
200				caller = st.Caller.Render(caller)
201				writeSpace(&l.b, firstKey)
202				l.b.WriteString(caller)
203			}
204		case PrefixKey:
205			if prefix, ok := keyvals[i+1].(string); ok {
206				prefix = st.Prefix.Render(prefix + ":")
207				writeSpace(&l.b, firstKey)
208				l.b.WriteString(prefix)
209			}
210		case MessageKey:
211			if msg := keyvals[i+1]; msg != nil {
212				m := fmt.Sprint(msg)
213				m = st.Message.Render(m)
214				writeSpace(&l.b, firstKey)
215				l.b.WriteString(m)
216			}
217		default:
218			sep := separator
219			indentSep := indentSeparator
220			sep = st.Separator.Render(sep)
221			indentSep = st.Separator.Render(indentSep)
222			key := fmt.Sprint(keyvals[i])
223			val := fmt.Sprintf("%+v", keyvals[i+1])
224			raw := val == ""
225			if raw {
226				val = `""`
227			}
228			if key == "" {
229				continue
230			}
231			actualKey := key
232			valueStyle := st.Value
233			if vs, ok := st.Values[actualKey]; ok {
234				valueStyle = vs
235			}
236			if keyStyle, ok := st.Keys[key]; ok {
237				key = keyStyle.Render(key)
238			} else {
239				key = st.Key.Render(key)
240			}
241
242			// Values may contain multiple lines, and that format
243			// is preserved, with each line prefixed with a "  | "
244			// to show it's part of a collection of lines.
245			//
246			// Values may also need quoting, if not all the runes
247			// in the value string are "normal", like if they
248			// contain ANSI escape sequences.
249			if strings.Contains(val, "\n") {
250				l.b.WriteString("\n  ")
251				l.b.WriteString(key)
252				l.b.WriteString(sep + "\n")
253				l.writeIndent(&l.b, val, indentSep, moreKeys, actualKey)
254			} else if !raw && needsQuoting(val) {
255				writeSpace(&l.b, firstKey)
256				l.b.WriteString(key)
257				l.b.WriteString(sep)
258				l.b.WriteString(valueStyle.Render(fmt.Sprintf(`"%s"`,
259					escapeStringForOutput(val, true))))
260			} else {
261				val = valueStyle.Render(val)
262				writeSpace(&l.b, firstKey)
263				l.b.WriteString(key)
264				l.b.WriteString(sep)
265				l.b.WriteString(val)
266			}
267		}
268	}
269
270	// Add a newline to the end of the log message.
271	l.b.WriteByte('\n')
272}