├── LICENSE ├── Readme.md ├── internal └── colors │ ├── colors.go │ └── colors_test.go ├── logformat.go └── logformat_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Log Format 2 | 3 | Some log formatting stuff, it's not super useful for other people. -------------------------------------------------------------------------------- /internal/colors/colors.go: -------------------------------------------------------------------------------- 1 | // Package colors provides some colors :) 2 | package colors 3 | 4 | import ( 5 | color "github.com/aybabtme/rgbterm" 6 | ) 7 | 8 | // None string. 9 | func None(s string) string { 10 | return s 11 | } 12 | 13 | // Gray string. 14 | func Gray(s string) string { 15 | return color.FgString(s, 150, 150, 150) 16 | } 17 | 18 | // Blue string. 19 | func Blue(s string) string { 20 | return color.FgString(s, 32, 115, 191) 21 | } 22 | 23 | // Cyan string. 24 | func Cyan(s string) string { 25 | return color.FgString(s, 25, 133, 152) 26 | } 27 | 28 | // Green string. 29 | func Green(s string) string { 30 | return color.FgString(s, 48, 137, 65) 31 | } 32 | 33 | // Red string. 34 | func Red(s string) string { 35 | return color.FgString(s, 194, 37, 92) 36 | } 37 | 38 | // Yellow string. 39 | func Yellow(s string) string { 40 | return color.FgString(s, 252, 196, 25) 41 | } 42 | 43 | // Purple string. 44 | func Purple(s string) string { 45 | return color.FgString(s, 96, 97, 190) 46 | } 47 | -------------------------------------------------------------------------------- /internal/colors/colors_test.go: -------------------------------------------------------------------------------- 1 | package colors_test 2 | -------------------------------------------------------------------------------- /logformat.go: -------------------------------------------------------------------------------- 1 | // Package logformat provides some ad-hoc log formatting for some of my projects. 2 | package logformat 3 | 4 | import ( 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/nqd/flat" 12 | "github.com/tj/go-logformat/internal/colors" 13 | ) 14 | 15 | // FormatFunc is a function used for formatting values. 16 | type FormatFunc func(string) string 17 | 18 | // DateFormatFunc is the function used for formatting dates. 19 | type DateFormatFunc func(time.Time) string 20 | 21 | // Formatters is a map of formatting functions. 22 | type Formatters map[string]FormatFunc 23 | 24 | // DefaultFormatters is a set of default formatters. 25 | var DefaultFormatters = Formatters{ 26 | // Levels. 27 | "debug": colors.Gray, 28 | "info": colors.Purple, 29 | "warn": colors.Yellow, 30 | "warning": colors.Yellow, 31 | "error": colors.Red, 32 | "fatal": colors.Red, 33 | "critical": colors.Red, 34 | "emergency": colors.Red, 35 | 36 | // Values. 37 | "string": colors.None, 38 | "number": colors.None, 39 | "bool": colors.None, 40 | "date": colors.Gray, 41 | 42 | // Fields. 43 | "object.key": colors.Purple, 44 | "object.separator": colors.Gray, 45 | "object.value": colors.None, 46 | 47 | // Arrays. 48 | "array.delimiter": colors.Gray, 49 | "array.separator": colors.Gray, 50 | 51 | // Special fields. 52 | "message": colors.None, 53 | "program": colors.Gray, 54 | "stage": colors.Gray, 55 | "version": colors.Gray, 56 | } 57 | 58 | // NoColor is a set of formatters resulting in no colors. 59 | var NoColor = Formatters{ 60 | // Levels. 61 | "debug": colors.None, 62 | "info": colors.None, 63 | "warn": colors.None, 64 | "warning": colors.None, 65 | "error": colors.None, 66 | "fatal": colors.None, 67 | "critical": colors.None, 68 | "emergency": colors.None, 69 | 70 | // Values. 71 | "string": colors.None, 72 | "number": colors.None, 73 | "bool": colors.None, 74 | "date": colors.None, 75 | 76 | // Fields. 77 | "object.key": colors.None, 78 | "object.separator": colors.None, 79 | "object.value": colors.None, 80 | 81 | // Arrays. 82 | "array.delimiter": colors.None, 83 | "array.separator": colors.None, 84 | 85 | // Special fields. 86 | "message": colors.None, 87 | "program": colors.None, 88 | "stage": colors.None, 89 | "version": colors.None, 90 | } 91 | 92 | // config is the formatter configuration. 93 | type config struct { 94 | format Formatters 95 | formatDate DateFormatFunc 96 | flatten bool 97 | prefix string 98 | } 99 | 100 | // Option function. 101 | type Option func(*config) 102 | 103 | // WithPrefix sets the prefix of each line. 104 | func WithPrefix(s string) Option { 105 | return func(c *config) { 106 | c.prefix = s 107 | } 108 | } 109 | 110 | // WithFormatters overrides the default formatters. 111 | func WithFormatters(v Formatters) Option { 112 | return func(c *config) { 113 | c.format = v 114 | } 115 | } 116 | 117 | // WithDateFormatter overrides the default date formatter. 118 | func WithDateFormatter(v DateFormatFunc) Option { 119 | return func(c *config) { 120 | c.formatDate = v 121 | } 122 | } 123 | 124 | // WithFlatten toggles flattening of fields. 125 | func WithFlatten(v bool) Option { 126 | return func(c *config) { 127 | c.flatten = v 128 | } 129 | } 130 | 131 | // newConfig returns config with options applied. 132 | func newConfig(options ...Option) *config { 133 | c := &config{ 134 | format: DefaultFormatters, 135 | formatDate: formatDate, 136 | } 137 | 138 | for _, o := range options { 139 | o(c) 140 | } 141 | 142 | return c 143 | } 144 | 145 | // Compact returns a value in the compact format. 146 | func Compact(m map[string]interface{}, options ...Option) string { 147 | return compact(m, newConfig(options...)) 148 | } 149 | 150 | // compact returns a formatted value. 151 | func compact(v interface{}, c *config) string { 152 | switch v := v.(type) { 153 | case map[string]interface{}: 154 | return compactMap(v, c) 155 | case []interface{}: 156 | return compactSlice(v, c) 157 | default: 158 | return primitive(v, c) 159 | } 160 | } 161 | 162 | // Prefix returns a prefix for log line special-casing, and removes those fields from the map. 163 | func Prefix(m map[string]interface{}, options ...Option) string { 164 | s := "" 165 | c := newConfig(options...) 166 | 167 | // timestamp 168 | if v, ok := m["timestamp"].(string); ok { 169 | t, err := time.Parse(time.RFC3339, v) 170 | if err == nil { 171 | s += primitive(t, c) + " " 172 | delete(m, "timestamp") 173 | } 174 | } 175 | 176 | // level 177 | if v, ok := m["level"].(string); ok { 178 | format := c.format[v] 179 | if format != nil { 180 | s += bold(format(strings.ToUpper(v[:4]))) + " " 181 | delete(m, "level") 182 | } 183 | } 184 | 185 | // application 186 | if v, ok := m["application"].(string); ok { 187 | s += c.format["application"](v) + " " 188 | delete(m, "application") 189 | } 190 | 191 | // stage 192 | if v, ok := m["stage"].(string); ok { 193 | s += c.format["stage"](v) + " " 194 | delete(m, "stage") 195 | } 196 | 197 | // version 198 | if v, ok := m["version"]; ok { 199 | switch v := v.(type) { 200 | case string: 201 | s += c.format["version"](v) + " " 202 | case float64: 203 | s += c.format["version"](strconv.FormatFloat(v, 'f', -1, 64)) + " " 204 | } 205 | delete(m, "version") 206 | } 207 | 208 | // message 209 | if v, ok := m["message"].(string); ok { 210 | s += c.format["message"](v) 211 | delete(m, "message") 212 | } 213 | 214 | return s 215 | } 216 | 217 | // compactMap returns a formatted map. 218 | func compactMap(m map[string]interface{}, c *config) string { 219 | m = maybeFlatten(m, c) 220 | s := "" 221 | keys := mapKeys(m) 222 | for i, k := range keys { 223 | v := m[k] 224 | s += c.format["object.key"](k) 225 | s += c.format["object.separator"]("=") 226 | if isComposite(v) { 227 | s += c.format["object.value"](compact(v, c)) 228 | } else { 229 | s += compact(v, c) 230 | } 231 | if i < len(keys)-1 { 232 | s += " " 233 | } 234 | } 235 | return s 236 | } 237 | 238 | // compactSlice returns a formatted slice. 239 | func compactSlice(v []interface{}, c *config) string { 240 | s := c.format["array.delimiter"]("[") 241 | for i, v := range v { 242 | if i > 0 { 243 | s += c.format["array.separator"](", ") 244 | } 245 | s += compact(v, c) 246 | } 247 | return s + c.format["array.delimiter"]("]") 248 | } 249 | 250 | // Expanded returns a value in the expanded format. 251 | func Expanded(m map[string]interface{}, options ...Option) string { 252 | c := newConfig(options...) 253 | return expanded(m, c.prefix, c) 254 | } 255 | 256 | // expanded returns a formatted value with prefix. 257 | func expanded(v interface{}, prefix string, c *config) string { 258 | switch v := v.(type) { 259 | case map[string]interface{}: 260 | return expandedMap(v, prefix, c) 261 | case []interface{}: 262 | return expandedSlice(v, prefix, c) 263 | default: 264 | return primitive(v, c) 265 | } 266 | } 267 | 268 | // expandedMap returns a formatted map. 269 | func expandedMap(m map[string]interface{}, prefix string, c *config) string { 270 | m = maybeFlatten(m, c) 271 | s := "" 272 | keys := mapKeys(m) 273 | for _, k := range keys { 274 | v := m[k] 275 | k = c.format["object.key"](k) 276 | d := c.format["object.separator"](":") 277 | if isComposite(v) { 278 | s += fmt.Sprintf("%s%s%s\n%s", prefix, k, d, expanded(v, prefix+" ", c)) 279 | } else { 280 | s += fmt.Sprintf("%s%s%s %s\n", prefix, k, d, expanded(v, prefix+" ", c)) 281 | } 282 | } 283 | return s 284 | } 285 | 286 | // expandedSlice returns a formatted slice. 287 | func expandedSlice(v []interface{}, prefix string, c *config) string { 288 | s := "" 289 | for _, v := range v { 290 | d := c.format["array.separator"]("-") 291 | if isComposite(v) { 292 | s += fmt.Sprintf("%s%s\n%s", prefix, d, expanded(v, prefix+" ", c)) 293 | } else { 294 | s += fmt.Sprintf("%s%s %v\n", prefix, d, primitive(v, c)) 295 | } 296 | } 297 | return s 298 | } 299 | 300 | // primitive returns a formatted value. 301 | func primitive(v interface{}, c *config) string { 302 | switch v := v.(type) { 303 | case string: 304 | if strings.ContainsAny(v, " \n\t") || strings.TrimSpace(v) == "" { 305 | return c.format["string"](strconv.Quote(v)) 306 | } else { 307 | return c.format["string"](v) 308 | } 309 | case time.Time: 310 | return c.format["date"](c.formatDate(v)) 311 | case bool: 312 | return c.format["bool"](strconv.FormatBool(v)) 313 | case float64: 314 | return c.format["number"](strconv.FormatFloat(v, 'f', -1, 64)) 315 | default: 316 | return fmt.Sprintf("%v", v) 317 | } 318 | } 319 | 320 | // isComposite returns true if the value is a composite. 321 | func isComposite(v interface{}) bool { 322 | switch v.(type) { 323 | case map[string]interface{}: 324 | return true 325 | case []interface{}: 326 | return true 327 | default: 328 | return false 329 | } 330 | } 331 | 332 | // mapKeys returns map keys, sorted ascending. 333 | func mapKeys(m map[string]interface{}) (keys []string) { 334 | for k := range m { 335 | keys = append(keys, k) 336 | } 337 | sort.Strings(keys) 338 | return 339 | } 340 | 341 | // formatDate formats t relative to now. 342 | func formatDate(t time.Time) string { 343 | return t.Format(`2006-01-02 15:04:05 MST`) 344 | } 345 | 346 | // bold string. 347 | func bold(s string) string { 348 | return fmt.Sprintf("\033[1m%s\033[0m", s) 349 | } 350 | 351 | // maybeFlatten returns a the original or flattened map when configured to do so. 352 | func maybeFlatten(m map[string]interface{}, c *config) map[string]interface{} { 353 | if c.flatten { 354 | m, _ = flat.Flatten(m, &flat.Options{Safe: true, Delimiter: "."}) 355 | } 356 | return m 357 | } 358 | -------------------------------------------------------------------------------- /logformat_test.go: -------------------------------------------------------------------------------- 1 | package logformat_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | logformat "github.com/tj/go-logformat" 9 | ) 10 | 11 | // newLog returns a new log. 12 | func newLog() map[string]interface{} { 13 | return map[string]interface{}{ 14 | // "timestamp": time.Now(), 15 | "timestamp": time.Now().Format(time.RFC3339), 16 | "message": "http request", 17 | "app": "up-api", 18 | "version": float64(27), 19 | "level": "info", 20 | "ip": "35.190.145.206", 21 | "plugin": "logs", 22 | "size": "7998", 23 | "id": "178f5348-304b-11e9-88af-9be94ad4eff3", 24 | "stage": "production", 25 | "duration": 359, 26 | "cart": map[string]interface{}{ 27 | "items": []interface{}{ 28 | map[string]interface{}{ 29 | "details": map[string]interface{}{ 30 | "name": "Ferret food", 31 | }, 32 | "cost": 10.99, 33 | }, 34 | map[string]interface{}{ 35 | "details": map[string]interface{}{ 36 | "name": "Cat food", 37 | }, 38 | "cost": 25.99, 39 | }, 40 | }, 41 | "total": 15.99, 42 | "paid": false, 43 | }, 44 | "pets": map[string]interface{}{ 45 | "list": []interface{}{ 46 | "Tobi", 47 | "Loki", 48 | "Jane", 49 | }, 50 | }, 51 | "method": "GET", 52 | "commit": "1d652f6", 53 | "path": "/install", 54 | "query": "", 55 | "region": "us-west-2", 56 | "status": "200", 57 | } 58 | } 59 | 60 | // Test prefix. 61 | func TestPrefix(t *testing.T) { 62 | log := newLog() 63 | fmt.Printf("%s — %s\n\n", logformat.Prefix(log), logformat.Compact(log)) 64 | } 65 | 66 | // Test compact logs. 67 | func TestCompact(t *testing.T) { 68 | log := newLog() 69 | fmt.Printf("%s\n\n", logformat.Compact(log)) 70 | } 71 | 72 | // Test compact logs with flattening. 73 | func TestCompact_WithFlatten(t *testing.T) { 74 | log := newLog() 75 | fmt.Printf("%s\n\n", logformat.Compact(log, logformat.WithFlatten(true))) 76 | } 77 | 78 | // Test expanded logs. 79 | func TestExpanded(t *testing.T) { 80 | log := newLog() 81 | fmt.Printf("%s\n", logformat.Expanded(log)) 82 | } 83 | 84 | // Test expanded logs with flattening. 85 | func TestExpanded_WithFlatten(t *testing.T) { 86 | log := newLog() 87 | fmt.Printf("%s\n", logformat.Expanded(log, logformat.WithFlatten(true))) 88 | } 89 | 90 | // Test expanded logs with prefix. 91 | func TestExpanded_WithPrefix(t *testing.T) { 92 | log := newLog() 93 | fmt.Printf("%s\n", logformat.Expanded(log, logformat.WithPrefix(" "))) 94 | } 95 | --------------------------------------------------------------------------------