@ -15,12 +15,14 @@ import (
const (
const (
timeFormat = "2006-01-02T15:04:05-0700"
timeFormat = "2006-01-02T15:04:05-0700"
termTimeFormat = "01-02|15:04:05.000"
floatFormat = 'f'
floatFormat = 'f'
termMsgJust = 40
termMsgJust = 40
termCtxMaxPadding = 40
termCtxMaxPadding = 40
)
)
// 40 spaces
var spaces = [ ] byte ( " " )
type Format interface {
type Format interface {
Format ( r slog . Record ) [ ] byte
Format ( r slog . Record ) [ ] byte
}
}
@ -44,37 +46,47 @@ type TerminalStringer interface {
TerminalString ( ) string
TerminalString ( ) string
}
}
func ( h * TerminalHandler ) TerminalFormat ( r slog . Record , usecolor bool ) [ ] byte {
func ( h * TerminalHandler ) TerminalFormat ( buf [ ] byte , r slog . Record , usecolor bool ) [ ] byte {
msg := escapeMessage ( r . Message )
msg := escapeMessage ( r . Message )
var color = 0
var color = ""
if usecolor {
if usecolor {
switch r . Level {
switch r . Level {
case LevelCrit :
case LevelCrit :
color = 35
color = "\x1b[35m"
case slog . LevelError :
case slog . LevelError :
color = 31
color = "\x1b[31m"
case slog . LevelWarn :
case slog . LevelWarn :
color = 33
color = "\x1b[33m"
case slog . LevelInfo :
case slog . LevelInfo :
color = 32
color = "\x1b[32m"
case slog . LevelDebug :
case slog . LevelDebug :
color = 36
color = "\x1b[36m"
case LevelTrace :
case LevelTrace :
color = 34
color = "\x1b[34m"
}
}
}
}
if buf == nil {
buf = make ( [ ] byte , 0 , 30 + termMsgJust )
}
b := bytes . NewBuffer ( buf )
b := & bytes . Buffer { }
if color != "" { // Start color
lvl := LevelAlignedString ( r . Level )
b . WriteString ( color )
if color > 0 {
b . WriteString ( LevelAlignedString ( r . Level ) )
fmt . Fprintf ( b , "\x1b[%dm%s\x1b[0m[%s] %s " , color , lvl , r . Time . Format ( termTimeFormat ) , msg )
b . WriteString ( "\x1b[0m" )
} else {
} else {
fmt . Fprintf ( b , "%s[%s] %s " , lvl , r . Time . Format ( termTimeFormat ) , msg )
b . WriteString ( LevelAlignedString ( r . Level ) )
}
}
b . WriteString ( "[" )
writeTimeTermFormat ( b , r . Time )
b . WriteString ( "] " )
b . WriteString ( msg )
// try to justify the log output for short messages
// try to justify the log output for short messages
length := utf8 . RuneCountInString ( msg )
//length := utf8.RuneCountInString(msg)
if r . NumAttrs ( ) > 0 && length < termMsgJust {
length := len ( msg )
b . Write ( bytes . Repeat ( [ ] byte { ' ' } , termMsgJust - length ) )
if ( r . NumAttrs ( ) + len ( h . attrs ) ) > 0 && length < termMsgJust {
b . Write ( spaces [ : termMsgJust - length ] )
}
}
// print the keys logfmt style
// print the keys logfmt style
h . logfmt ( b , r , color )
h . logfmt ( b , r , color )
@ -82,150 +94,139 @@ func (h *TerminalHandler) TerminalFormat(r slog.Record, usecolor bool) []byte {
return b . Bytes ( )
return b . Bytes ( )
}
}
func ( h * TerminalHandler ) logfmt ( buf * bytes . Buffer , r slog . Record , color int ) {
func ( h * TerminalHandler ) logfmt ( buf * bytes . Buffer , r slog . Record , color string ) {
attrs := [ ] slog . Attr { }
// tmp is a temporary buffer we use, until bytes.Buffer.AvailableBuffer() (1.21)
r . Attrs ( func ( attr slog . Attr ) bool {
// can be used.
attrs = append ( attrs , attr )
var tmp = make ( [ ] byte , 40 )
return true
writeAttr := func ( attr slog . Attr , first , last bool ) {
} )
buf . WriteByte ( ' ' )
attrs = append ( h . attrs , attrs ... )
if color != "" {
buf . WriteString ( color )
for i , attr := range attrs {
//buf.Write(appendEscapeString(buf.AvailableBuffer(), attr.Key))
if i != 0 {
buf . Write ( appendEscapeString ( tmp [ : 0 ] , attr . Key ) )
buf . WriteByte ( ' ' )
buf . WriteString ( "\x1b[0m=" )
} else {
//buf.Write(appendEscapeString(buf.AvailableBuffer(), attr.Key))
buf . Write ( appendEscapeString ( tmp [ : 0 ] , attr . Key ) )
buf . WriteByte ( '=' )
}
}
//val := FormatSlogValue(attr.Value, true, buf.AvailableBuffer())
val := FormatSlogValue ( attr . Value , true , tmp [ : 0 ] )
key := escapeString ( attr . Key )
padding := h . fieldPadding [ attr . Key ]
rawVal := attr . Value . Any ( )
val := FormatLogfmtValue ( rawVal , true )
// XXX: we should probably check that all of your key bytes aren't invalid
length := utf8 . RuneCount ( val )
// TODO (jwasinger) above comment was from log15 code. what does it mean? check that key bytes are ascii characters?
padding := h . fieldPadding [ key ]
length := utf8 . RuneCountInString ( val )
if padding < length && length <= termCtxMaxPadding {
if padding < length && length <= termCtxMaxPadding {
padding = length
padding = length
h . fieldPadding [ k ey] = padding
h . fieldPadding [ attr . Key ] = padding
}
}
if color > 0 {
buf . Write ( val )
fmt . Fprintf ( buf , "\x1b[%dm%s\x1b[0m=" , color , key )
if ! last && padding > length {
} else {
buf . Write ( spaces [ : padding - length ] )
buf . WriteString ( key )
buf . WriteByte ( '=' )
}
buf . WriteString ( val )
if i < r . NumAttrs ( ) - 1 && padding > length {
buf . Write ( bytes . Repeat ( [ ] byte { ' ' } , padding - length ) )
}
}
}
}
var n = 0
var nAttrs = len ( h . attrs ) + r . NumAttrs ( )
for _ , attr := range h . attrs {
writeAttr ( attr , n == 0 , n == nAttrs - 1 )
n ++
}
r . Attrs ( func ( attr slog . Attr ) bool {
writeAttr ( attr , n == 0 , n == nAttrs - 1 )
n ++
return true
} )
buf . WriteByte ( '\n' )
buf . WriteByte ( '\n' )
}
}
// formatValue formats a value for serialization
// FormatSlogValue formats a slog.Value for serialization
func FormatLogfmtValue ( value interface { } , term bool ) ( result string ) {
func FormatSlogValue ( v slog . Value , term bool , tmp [ ] byte ) ( result [ ] byte ) {
if value == nil {
var value any
return "<nil>"
}
defer func ( ) {
defer func ( ) {
if err := recover ( ) ; err != nil {
if err := recover ( ) ; err != nil {
if v := reflect . ValueOf ( value ) ; v . Kind ( ) == reflect . Ptr && v . IsNil ( ) {
if v := reflect . ValueOf ( value ) ; v . Kind ( ) == reflect . Ptr && v . IsNil ( ) {
result = "<nil>"
result = [ ] byte ( "<nil>" )
} else {
} else {
panic ( err )
panic ( err )
}
}
}
}
} ( )
} ( )
switch v := value . ( type ) {
switch v . Kind ( ) {
case time . Time :
case slog . KindString :
return appendEscapeString ( tmp , v . String ( ) )
case slog . KindAny :
value = v . Any ( )
case slog . KindInt64 : // All int-types (int8 ,int16 etc) wind up here
return appendInt64 ( tmp , v . Int64 ( ) )
case slog . KindUint64 : // All uint-types (int8 ,int16 etc) wind up here
return appendUint64 ( tmp , v . Uint64 ( ) , false )
case slog . KindFloat64 :
return strconv . AppendFloat ( tmp , v . Float64 ( ) , floatFormat , 3 , 64 )
case slog . KindBool :
return strconv . AppendBool ( tmp , v . Bool ( ) )
case slog . KindDuration :
value = v . Duration ( )
case slog . KindTime :
// Performance optimization: No need for escaping since the provided
// Performance optimization: No need for escaping since the provided
// timeFormat doesn't have any escape characters, and escaping is
// timeFormat doesn't have any escape characters, and escaping is
// expensive.
// expensive.
return v . Format ( timeFormat )
return v . Time ( ) . AppendFormat ( tmp , timeFormat )
default :
value = v . Any ( )
}
if value == nil {
return [ ] byte ( "<nil>" )
}
switch v := value . ( type ) {
case * big . Int :
case * big . Int :
// Big ints get consumed by the Stringer clause, so we need to handle
// Big ints get consumed by the Stringer clause, so we need to handle
// them earlier on.
// them earlier on.
if v == nil {
if v == nil {
return "<nil>"
return append ( tmp , [ ] byte ( "<nil>" ) ... )
}
}
return formatLogfmtBigInt ( v )
return appendBigInt ( tmp , v )
case * uint256 . Int :
case * uint256 . Int :
// Uint256s get consumed by the Stringer clause, so we need to handle
// Uint256s get consumed by the Stringer clause, so we need to handle
// them earlier on.
// them earlier on.
if v == nil {
if v == nil {
return "<nil>"
return append ( tmp , [ ] byte ( "<nil>" ) ... )
}
return formatLogfmtUint256 ( v )
}
if term {
if s , ok := value . ( TerminalStringer ) ; ok {
// Custom terminal stringer provided, use that
return escapeString ( s . TerminalString ( ) )
}
}
}
return appendU256 ( tmp , v )
switch v := value . ( type ) {
case error :
case error :
return escapeString ( v . Error ( ) )
return appendEscapeString ( tmp , v . Error ( ) )
case TerminalStringer :
if term {
return appendEscapeString ( tmp , v . TerminalString ( ) ) // Custom terminal stringer provided, use that
}
case fmt . Stringer :
case fmt . Stringer :
return escapeString ( v . String ( ) )
return appendEscapeString ( tmp , v . String ( ) )
case bool :
return strconv . FormatBool ( v )
case float32 :
return strconv . FormatFloat ( float64 ( v ) , floatFormat , 3 , 64 )
case float64 :
return strconv . FormatFloat ( v , floatFormat , 3 , 64 )
case int8 :
return strconv . FormatInt ( int64 ( v ) , 10 )
case uint8 :
return strconv . FormatInt ( int64 ( v ) , 10 )
case int16 :
return strconv . FormatInt ( int64 ( v ) , 10 )
case uint16 :
return strconv . FormatInt ( int64 ( v ) , 10 )
// Larger integers get thousands separators.
case int :
return FormatLogfmtInt64 ( int64 ( v ) )
case int32 :
return FormatLogfmtInt64 ( int64 ( v ) )
case int64 :
return FormatLogfmtInt64 ( v )
case uint :
return FormatLogfmtUint64 ( uint64 ( v ) )
case uint32 :
return FormatLogfmtUint64 ( uint64 ( v ) )
case uint64 :
return FormatLogfmtUint64 ( v )
case string :
return escapeString ( v )
default :
return escapeString ( fmt . Sprintf ( "%+v" , value ) )
}
}
// We can use the 'tmp' as a scratch-buffer, to first format the
// value, and in a second step do escaping.
internal := fmt . Appendf ( tmp , "%+v" , value )
return appendEscapeString ( tmp , string ( internal ) )
}
}
// FormatLogfmtInt64 formats n with thousand separators.
// appendInt64 formats n with thousand separators and writes into buffer dst.
func FormatLogfmtInt64 ( n int64 ) string {
func appendInt64 ( dst [ ] byte , n int64 ) [ ] byte {
if n < 0 {
if n < 0 {
return formatLogfmtUint64 ( uint64 ( - n ) , true )
return appendUint64 ( dst , uint64 ( - n ) , true )
}
}
return formatLogfmtUint64 ( uint64 ( n ) , false )
return appendUint64 ( dst , uint64 ( n ) , false )
}
// FormatLogfmtUint64 formats n with thousand separators.
func FormatLogfmtUint64 ( n uint64 ) string {
return formatLogfmtUint64 ( n , false )
}
}
func formatLogfmtUint64 ( n uint64 , neg bool ) string {
// appendUint64 formats n with thousand separators and writes into buffer dst.
func appendUint64 ( dst [ ] byte , n uint64 , neg bool ) [ ] byte {
// Small numbers are fine as is
// Small numbers are fine as is
if n < 100000 {
if n < 100000 {
if neg {
if neg {
return strconv . Itoa ( - int ( n ) )
return strconv . AppendInt ( dst , - int64 ( n ) , 10 )
} else {
} else {
return strconv . Itoa ( int ( n ) )
return strconv . AppendInt ( dst , int64 ( n ) , 10 )
}
}
}
}
// Large numbers should be split
// Large numbers should be split
@ -250,16 +251,21 @@ func formatLogfmtUint64(n uint64, neg bool) string {
out [ i ] = '-'
out [ i ] = '-'
i --
i --
}
}
return string ( out [ i + 1 : ] )
return append ( dst , out [ i + 1 : ] ... )
}
}
// formatLogfmtBigInt formats n with thousand separators.
// FormatLogfmtUint64 formats n with thousand separators.
func formatLogfmtBigInt ( n * big . Int ) string {
func FormatLogfmtUint64 ( n uint64 ) string {
return string ( appendUint64 ( nil , n , false ) )
}
// appendBigInt formats n with thousand separators and writes to dst.
func appendBigInt ( dst [ ] byte , n * big . Int ) [ ] byte {
if n . IsUint64 ( ) {
if n . IsUint64 ( ) {
return FormatLogfmtUint64 ( n . Uint64 ( ) )
return append Uint64( dst , n . Uint64 ( ) , false )
}
}
if n . IsInt64 ( ) {
if n . IsInt64 ( ) {
return FormatLogfmtInt64 ( n . Int64 ( ) )
return appendInt64 ( dst , n . Int64 ( ) )
}
}
var (
var (
@ -284,54 +290,48 @@ func formatLogfmtBigInt(n *big.Int) string {
comma ++
comma ++
}
}
}
}
return string ( buf [ i + 1 : ] )
return append ( dst , buf [ i + 1 : ] ... )
}
}
// formatLogfmtUint 256 formats n with thousand separators.
// appendU 256 formats n with thousand separators.
func formatLogfmtUint 256( n * uint256 . Int ) string {
func appendU 256( dst [ ] byte , n * uint256 . Int ) [ ] byte {
if n . IsUint64 ( ) {
if n . IsUint64 ( ) {
return FormatLogfmt Uint64( n . Uint64 ( ) )
return append Uint64( dst , n . Uint64 ( ) , false )
}
}
var (
res := [ ] byte ( n . PrettyDec ( ',' ) )
text = n . Dec ( )
return append ( dst , res ... )
buf = make ( [ ] byte , len ( text ) + len ( text ) / 3 )
comma = 0
i = len ( buf ) - 1
)
for j := len ( text ) - 1 ; j >= 0 ; j , i = j - 1 , i - 1 {
c := text [ j ]
switch {
case c == '-' :
buf [ i ] = c
case comma == 3 :
buf [ i ] = ','
i --
comma = 0
fallthrough
default :
buf [ i ] = c
comma ++
}
}
return string ( buf [ i + 1 : ] )
}
}
// escapeString checks if the provided string needs escaping/quoting, and
// appendEscapeString writes the string s to the given writer, with
// calls strconv.Quote if needed
// escaping/quoting if needed.
func escapeString ( s string ) string {
func appendEscapeString ( dst [ ] byte , s string ) [ ] byte {
needsQuoting := false
needsQuoting := false
needsEscaping := false
for _ , r := range s {
for _ , r := range s {
// We quote everything below " (0x22) and above~ (0x7E), plus equal-sign
// If it contains spaces or equal-sign, we need to quote it.
if r <= '"' || r > '~ ' || r == '=' {
if r == ' ' || r == '=' {
needsQuoting = true
needsQuoting = true
continue
}
// We need to escape it, if it contains
// - character " (0x22) and lower (except space)
// - characters above ~ (0x7E), plus equal-sign
if r <= '"' || r > '~' {
needsEscaping = true
break
break
}
}
}
}
if ! needsQuoting {
if needsEscap ing {
return s
return strconv . AppendQuote ( dst , s )
}
}
return strconv . Quote ( s )
// No escaping needed, but we might have to place within quote-marks, in case
// it contained a space
if needsQuoting {
dst = append ( dst , '"' )
dst = append ( dst , [ ] byte ( s ) ... )
return append ( dst , '"' )
}
return append ( dst , [ ] byte ( s ) ... )
}
}
// escapeMessage checks if the provided string needs escaping/quoting, similarly
// escapeMessage checks if the provided string needs escaping/quoting, similarly
@ -356,3 +356,45 @@ func escapeMessage(s string) string {
}
}
return strconv . Quote ( s )
return strconv . Quote ( s )
}
}
// writeTimeTermFormat writes on the format "01-02|15:04:05.000"
func writeTimeTermFormat ( buf * bytes . Buffer , t time . Time ) {
_ , month , day := t . Date ( )
writePosIntWidth ( buf , int ( month ) , 2 )
buf . WriteByte ( '-' )
writePosIntWidth ( buf , day , 2 )
buf . WriteByte ( '|' )
hour , min , sec := t . Clock ( )
writePosIntWidth ( buf , hour , 2 )
buf . WriteByte ( ':' )
writePosIntWidth ( buf , min , 2 )
buf . WriteByte ( ':' )
writePosIntWidth ( buf , sec , 2 )
ns := t . Nanosecond ( )
buf . WriteByte ( '.' )
writePosIntWidth ( buf , ns / 1e6 , 3 )
}
// writePosIntWidth writes non-negative integer i to the buffer, padded on the left
// by zeroes to the given width. Use a width of 0 to omit padding.
// Adapted from golang.org/x/exp/slog/internal/buffer/buffer.go
func writePosIntWidth ( b * bytes . Buffer , i , width int ) {
// Cheap integer to fixed-width decimal ASCII.
// Copied from log/log.go.
if i < 0 {
panic ( "negative int" )
}
// Assemble decimal in reverse order.
var bb [ 20 ] byte
bp := len ( bb ) - 1
for i >= 10 || width > 1 {
width --
q := i / 10
bb [ bp ] = byte ( '0' + i - q * 10 )
bp --
i = q
}
// i < 10
bb [ bp ] = byte ( '0' + i )
b . Write ( bb [ bp : ] )
}