From 3e4a6c3d072da3b0fa6f84c116325b887514b344 Mon Sep 17 00:00:00 2001 From: Peter Edge Date: Mon, 6 Mar 2017 13:49:28 -0500 Subject: [PATCH] Add colored LevelEncoders (#307) First, create a small package for TTY coloring. Then, use the internal package to add small, colored LevelEncoders. For safety (since we don't check that the output is a TTY), keep the default uncolored. --- Makefile | 2 +- internal/color/color.go | 44 +++++++++++++++++++++++++++++++++ internal/color/color_test.go | 36 +++++++++++++++++++++++++++ zapcore/encoder.go | 31 ++++++++++++++++++++--- zapcore/level.go | 24 ++++++++++++++++++ zapcore/level_strings.go | 46 +++++++++++++++++++++++++++++++++++ zapcore/level_strings_test.go | 38 +++++++++++++++++++++++++++++ zapcore/level_test.go | 3 ++- 8 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 internal/color/color.go create mode 100644 internal/color/color_test.go create mode 100644 zapcore/level_strings.go create mode 100644 zapcore/level_strings_test.go diff --git a/Makefile b/Makefile index be388ba9f..fb0409698 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ export GO15VENDOREXPERIMENT=1 BENCH_FLAGS ?= -cpuprofile=cpu.pprof -memprofile=mem.pprof -benchmem PKGS ?= $(shell glide novendor) # Many Go tools take file globs or directories as arguments instead of packages. -PKG_FILES ?= *.go zapcore benchmarks buffer testutils internal/bufferpool internal/exit internal/multierror internal/observer +PKG_FILES ?= *.go zapcore benchmarks buffer testutils internal/bufferpool internal/exit internal/multierror internal/observer internal/color # The linting tools evolve with each Go version, so run them only on the latest # stable release. diff --git a/internal/color/color.go b/internal/color/color.go new file mode 100644 index 000000000..c4d5d02ab --- /dev/null +++ b/internal/color/color.go @@ -0,0 +1,44 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package color adds coloring functionality for TTY output. +package color + +import "fmt" + +// Foreground colors. +const ( + Black Color = iota + 30 + Red + Green + Yellow + Blue + Magenta + Cyan + White +) + +// Color represents a text color. +type Color uint8 + +// Add adds the coloring to the given string. +func (c Color) Add(s string) string { + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(c), s) +} diff --git a/internal/color/color_test.go b/internal/color/color_test.go new file mode 100644 index 000000000..4982903aa --- /dev/null +++ b/internal/color/color_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package color + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestColorFormatting(t *testing.T) { + assert.Equal( + t, + "\x1b[31mfoo\x1b[0m", + Red.Add("foo"), + "Unexpected colored output.", + ) +} diff --git a/zapcore/encoder.go b/zapcore/encoder.go index 86cbd194d..1bb684ae3 100644 --- a/zapcore/encoder.go +++ b/zapcore/encoder.go @@ -21,7 +21,6 @@ package zapcore import ( - "strings" "time" "go.uber.org/zap/buffer" @@ -36,18 +35,44 @@ func LowercaseLevelEncoder(l Level, enc PrimitiveArrayEncoder) { enc.AppendString(l.String()) } +// LowercaseColorLevelEncoder serializes a Level to a lowercase string and adds coloring. +// For example, InfoLevel is serialized to "info" and colored blue. +func LowercaseColorLevelEncoder(l Level, enc PrimitiveArrayEncoder) { + s, ok := _levelToLowercaseColorString[l] + if !ok { + s = _unknownLevelColor.Add(l.String()) + } + enc.AppendString(s) +} + // CapitalLevelEncoder serializes a Level to an all-caps string. For example, // InfoLevel is serialized to "INFO". func CapitalLevelEncoder(l Level, enc PrimitiveArrayEncoder) { - enc.AppendString(strings.ToUpper(l.String())) + enc.AppendString(l.CapitalString()) +} + +// CapitalColorLevelEncoder serializes a Level to an all-caps string and adds color. +// For example, InfoLevel is serialized to "INFO" and colored blue. +func CapitalColorLevelEncoder(l Level, enc PrimitiveArrayEncoder) { + s, ok := _levelToCapitalColorString[l] + if !ok { + s = _unknownLevelColor.Add(l.CapitalString()) + } + enc.AppendString(s) } // UnmarshalText unmarshals text to a LevelEncoder. "capital" is unmarshaled to -// CapitalLevelEncoder, and anything else is unmarshaled to LowercaseLevelEncoder. +// CapitalLevelEncoder, "coloredCapital" is unmarshaled to CapitalColorLevelEncoder, +// "colored" is unmarshaled to LowercaseColorLevelEncoder, and anything else +// is unmarshaled to LowercaseLevelEncoder. func (e *LevelEncoder) UnmarshalText(text []byte) error { switch string(text) { case "capital": *e = CapitalLevelEncoder + case "capitalColor": + *e = CapitalColorLevelEncoder + case "color": + *e = LowercaseColorLevelEncoder default: *e = LowercaseLevelEncoder } diff --git a/zapcore/level.go b/zapcore/level.go index 763b44b06..4997f0aca 100644 --- a/zapcore/level.go +++ b/zapcore/level.go @@ -76,6 +76,30 @@ func (l Level) String() string { } } +// CapitalString returns an all-caps ASCII representation of the log level. +func (l Level) CapitalString() string { + // Printing levels in all-caps is common enough that we should export this + // functionality. + switch l { + case DebugLevel: + return "DEBUG" + case InfoLevel: + return "INFO" + case WarnLevel: + return "WARN" + case ErrorLevel: + return "ERROR" + case DPanicLevel: + return "DPANIC" + case PanicLevel: + return "PANIC" + case FatalLevel: + return "FATAL" + default: + return fmt.Sprintf("LEVEL(%d)", l) + } +} + // MarshalText marshals the Level to text. Note that the text representation // drops the -Level suffix (see example). func (l *Level) MarshalText() ([]byte, error) { diff --git a/zapcore/level_strings.go b/zapcore/level_strings.go new file mode 100644 index 000000000..7af8dadcb --- /dev/null +++ b/zapcore/level_strings.go @@ -0,0 +1,46 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import "go.uber.org/zap/internal/color" + +var ( + _levelToColor = map[Level]color.Color{ + DebugLevel: color.Magenta, + InfoLevel: color.Blue, + WarnLevel: color.Yellow, + ErrorLevel: color.Red, + DPanicLevel: color.Red, + PanicLevel: color.Red, + FatalLevel: color.Red, + } + _unknownLevelColor = color.Red + + _levelToLowercaseColorString = make(map[Level]string, len(_levelToColor)) + _levelToCapitalColorString = make(map[Level]string, len(_levelToColor)) +) + +func init() { + for level, color := range _levelToColor { + _levelToLowercaseColorString[level] = color.Add(level.String()) + _levelToCapitalColorString[level] = color.Add(level.CapitalString()) + } +} diff --git a/zapcore/level_strings_test.go b/zapcore/level_strings_test.go new file mode 100644 index 000000000..14b0bac62 --- /dev/null +++ b/zapcore/level_strings_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2016 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package zapcore + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAllLevelsCoveredByLevelString(t *testing.T) { + numLevels := int((_maxLevel - _minLevel) + 1) + + isComplete := func(m map[Level]string) bool { + return len(m) == numLevels + } + + assert.True(t, isComplete(_levelToLowercaseColorString), "Colored lowercase strings don't cover all levels.") + assert.True(t, isComplete(_levelToCapitalColorString), "Colored capital strings don't cover all levels.") +} diff --git a/zapcore/level_test.go b/zapcore/level_test.go index 37bbbf9c5..b711b797b 100644 --- a/zapcore/level_test.go +++ b/zapcore/level_test.go @@ -42,7 +42,8 @@ func TestLevelString(t *testing.T) { } for lvl, stringLevel := range tests { - assert.Equal(t, stringLevel, lvl.String()) + assert.Equal(t, stringLevel, lvl.String(), "Unexpected lowercase level string.") + assert.Equal(t, strings.ToUpper(stringLevel), lvl.CapitalString(), "Unexpected all-caps level string.") } }