github.com/emc-advanced-dev/unik@v0.0.0-20190717152701-a58d3e8e33b7/pkg/util/log_formatter.go (about) 1 package util 2 3 import ( 4 "bytes" 5 "fmt" 6 "runtime" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/sirupsen/logrus" 12 ) 13 14 //Redaction formatter 15 16 const ( 17 nocolor = 0 18 red = 31 19 green = 32 20 yellow = 33 21 blue = 34 22 gray = 37 23 ) 24 25 var ( 26 baseTimestamp time.Time 27 isTerminal bool 28 ) 29 30 func init() { 31 baseTimestamp = time.Now() 32 } 33 34 func miniTS() int { 35 return int(time.Since(baseTimestamp) / time.Second) 36 } 37 38 type RedactedTextFormatter struct { 39 // Set to true to bypass checking for a TTY before outputting colors. 40 ForceColors bool 41 42 // Force disabling colors. 43 DisableColors bool 44 45 // Disable timestamp logging. useful when output is redirected to logging 46 // system that already adds timestamps. 47 DisableTimestamp bool 48 49 // Enable logging the full timestamp when a TTY is attached instead of just 50 // the time passed since beginning of execution. 51 FullTimestamp bool 52 53 // TimestampFormat to use for display when a full timestamp is printed 54 TimestampFormat string 55 56 // The fields are sorted by default for a consistent output. For applications 57 // that log extremely frequently and don't use the JSON formatter this may not 58 // be desired. 59 DisableSorting bool 60 61 // Redactions specifies sensitive strings that should be redacted (replaced 62 // with *'s) in all outputs. 63 Redactions []string 64 } 65 66 func (f *RedactedTextFormatter) Format(entry *logrus.Entry) ([]byte, error) { 67 var keys []string = make([]string, 0, len(entry.Data)) 68 for k := range entry.Data { 69 keys = append(keys, k) 70 } 71 72 if !f.DisableSorting { 73 sort.Strings(keys) 74 } 75 76 b := &bytes.Buffer{} 77 78 prefixFieldClashes(entry.Data) 79 80 isColorTerminal := isTerminal && (runtime.GOOS != "windows") 81 isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors 82 83 timestampFormat := f.TimestampFormat 84 if timestampFormat == "" { 85 timestampFormat = time.RFC3339 86 } 87 if isColored { 88 f.printColored(b, entry, keys, timestampFormat) 89 } else { 90 if !f.DisableTimestamp { 91 f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat)) 92 } 93 f.appendKeyValue(b, "level", entry.Level.String()) 94 if entry.Message != "" { 95 f.appendKeyValue(b, "msg", entry.Message) 96 } 97 for _, key := range keys { 98 f.appendKeyValue(b, key, entry.Data[key]) 99 } 100 } 101 102 //redact here 103 written := b.String() 104 b.Reset() 105 for _, redactStr := range f.Redactions { 106 written = Redact(written, redactStr) 107 } 108 b.WriteString(written) 109 110 b.WriteByte('\n') 111 return b.Bytes(), nil 112 } 113 114 func (f *RedactedTextFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry, keys []string, timestampFormat string) { 115 var levelColor int 116 switch entry.Level { 117 case logrus.DebugLevel: 118 levelColor = gray 119 case logrus.WarnLevel: 120 levelColor = yellow 121 case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel: 122 levelColor = red 123 default: 124 levelColor = blue 125 } 126 127 levelText := strings.ToUpper(entry.Level.String())[0:4] 128 129 if !f.FullTimestamp { 130 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) 131 } else { 132 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) 133 } 134 for _, k := range keys { 135 v := entry.Data[k] 136 fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v) 137 } 138 } 139 140 func needsQuoting(text string) bool { 141 for _, ch := range text { 142 if !((ch >= 'a' && ch <= 'z') || 143 (ch >= 'A' && ch <= 'Z') || 144 (ch >= '0' && ch <= '9') || 145 ch == '-' || ch == '.') { 146 return false 147 } 148 } 149 return true 150 } 151 152 func (f *RedactedTextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { 153 154 b.WriteString(key) 155 b.WriteByte('=') 156 157 switch value := value.(type) { 158 case string: 159 if needsQuoting(value) { 160 b.WriteString(value) 161 } else { 162 fmt.Fprintf(b, "%q", value) 163 } 164 case error: 165 errmsg := value.Error() 166 if needsQuoting(errmsg) { 167 b.WriteString(errmsg) 168 } else { 169 fmt.Fprintf(b, "%q", value) 170 } 171 default: 172 fmt.Fprint(b, value) 173 } 174 175 b.WriteByte(' ') 176 } 177 178 // This is to not silently overwrite `time`, `msg` and `level` fields when 179 // dumping it. If this code wasn't there doing: 180 // 181 // logrus.WithField("level", 1).Info("hello") 182 // 183 // Would just silently drop the user provided level. Instead with this code 184 // it'll logged as: 185 // 186 // {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} 187 // 188 // It's not exported because it's still using Data in an opinionated way. It's to 189 // avoid code duplication between the two default formatters. 190 func prefixFieldClashes(data logrus.Fields) { 191 _, ok := data["time"] 192 if ok { 193 data["fields.time"] = data["time"] 194 } 195 196 _, ok = data["msg"] 197 if ok { 198 data["fields.msg"] = data["msg"] 199 } 200 201 _, ok = data["level"] 202 if ok { 203 data["fields.level"] = data["level"] 204 } 205 } 206 207 func Redact(str, substr string) string { 208 return strings.Replace(str, substr, strings.Repeat("*", len(substr)), -1) 209 }