b0b0654d0ed14f454bebe9f3b54d422d607c53b0

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add force color option

Diff

  1diff --git a/README.md b/README.md
  2index b73c23f2700d79e83a7a1d117c87b6b9ae6c3b23..fad24f692ca9c155e88d19eb5055bec2c8ead75d 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -32,16 +32,17 @@ go install github.com/TheEdgeOfRage/logfmt
  6 
  7 ```
  8 Usage:
  9-  logfmt [OPTIONS]
 10+  main [OPTIONS]
 11 
 12 Application Options:
 13-  -l, --level=    Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL (default: INFO)
 14-  -o, --output=   Output field selector (comma separated)
 15-  -f, --filter=   Filter fields (key=value comma separated)
 16-  -n, --no-color  Disable color output
 17+  -l, --level=       Log level filter. One of DEBUG, INFO, WARN, ERROR, FATAL (default: INFO)
 18+  -o, --output=      Output field selector (comma separated)
 19+  -f, --filter=      Filter fields (key=value comma separated)
 20+  -n, --no-color     Disable color output
 21+  -c, --force-color  Force color output, even when outputting to a pipe
 22 
 23 Help Options:
 24-  -h, --help      Show this help message
 25+  -h, --help         Show this help message
 26 ```
 27 
 28 If installed in your PATH, you can just run the `logfmt` program without any arguments and it will start reading log
 29@@ -50,6 +51,13 @@ lines from stdin and write the formatted lines to stdout.
 30 This CLI follows the UNIX philosophy, so it will only read from stdin and write to stdout. If you want stderr or a different
 31 file, use your shell's built-in directives for that.
 32 
 33+A typical usecase would be running a service for local development, or fetching the logs from a Kubernetes pod:
 34+
 35+```
 36+go run yourservice.go | logfmt
 37+kubectl logs -n namespace pod | logfmt
 38+```
 39+
 40 #### Level filtering
 41 
 42 To filters your logs based on the log level, you can pass the `-l` flag with a log level in CAPS format. The level you
 43@@ -69,4 +77,9 @@ numerical filtering might come in the future.
 44 
 45 #### No color
 46 
 47-If you don't want to have colors on the output, use `-n`.
 48+If you don't want to have colors on the output, set `-n`.
 49+
 50+#### Force color
 51+
 52+By default, logfmt will detect if the output is a pipe or redirect to a file and will automatically disable colors. If
 53+you still want to have colorized output, for example when piping into `less`, you can force it using `-c`.
 54diff --git a/config/config.go b/config/config.go
 55index d1f5ce535f6e6547215088fcefe12a7c0f3d6ddb..0ab3ee2877aff3aa1115430c562ad28fe8d77cde 100644
 56--- a/config/config.go
 57+++ b/config/config.go
 58@@ -4,7 +4,8 @@ import (
 59 	"fmt"
 60 	"strings"
 61 
 62-	flags "github.com/jessevdk/go-flags"
 63+	"github.com/fatih/color"
 64+	"github.com/jessevdk/go-flags"
 65 )
 66 
 67 const (
 68@@ -22,8 +23,6 @@ type Config struct {
 69 	OutputFields []string
 70 	// Filter is a map of fields and values which are used to filter the log output
 71 	Filter map[string]string
 72-	// Color is a flag to enable or disable color on the output
 73-	Color bool
 74 }
 75 
 76 type rawConfig struct {
 77@@ -31,6 +30,7 @@ type rawConfig struct {
 78 	OutputFields string `long:"output" short:"o" description:"Output field selector (comma separated)"`
 79 	Filter       string `long:"filter" short:"f" description:"Filter fields (key=value comma separated)"`
 80 	NoColor      bool   `long:"no-color" short:"n" description:"Disable color output"`
 81+	ForceColor   bool   `long:"force-color" short:"c" description:"Force color output, even when outputting to a pipe"`
 82 }
 83 
 84 func Parse() (*Config, error) {
 85@@ -42,9 +42,7 @@ func Parse() (*Config, error) {
 86 		return nil, err
 87 	}
 88 
 89-	cfg := Config{
 90-		Color: !raw.NoColor,
 91-	}
 92+	cfg := Config{}
 93 	cfg.setOutputFields(raw.OutputFields)
 94 	err = cfg.setFilter(raw.Filter)
 95 	if err != nil {
 96@@ -54,6 +52,15 @@ func Parse() (*Config, error) {
 97 	if err != nil {
 98 		return nil, err
 99 	}
100+	if raw.ForceColor && raw.NoColor {
101+		return nil, fmt.Errorf("cannot use both --force-color and --no-color")
102+	}
103+	if raw.ForceColor {
104+		color.NoColor = false
105+	}
106+	if raw.NoColor {
107+		color.NoColor = true
108+	}
109 
110 	return &cfg, nil
111 }
112diff --git a/parser/parser.go b/parser/parser.go
113index 75cc148e190122cd62c009806eb7c4ff0c18295d..ff76b7045abd78f6a5cff198efd7ccafbb872ab5 100644
114--- a/parser/parser.go
115+++ b/parser/parser.go
116@@ -37,7 +37,7 @@ func (p *Parser) Start() error {
117 			continue
118 		}
119 
120-		_, err = fmt.Fprintf(p.output, "%s\n", record.String(p.cfg.OutputFields, p.cfg.Color))
121+		_, err = fmt.Fprintf(p.output, "%s\n", record.String(p.cfg.OutputFields))
122 		if err != nil {
123 			return fmt.Errorf("failed to print log to output: %w", err)
124 		}
125diff --git a/parser/record.go b/parser/record.go
126index 1e4da18aa5c7cb7e6d2f30cf1b13ef3b0781f211..fcc34e805523b304f7857b1a797e84dddc4bccae 100644
127--- a/parser/record.go
128+++ b/parser/record.go
129@@ -86,28 +86,21 @@ func isNull(s string) bool {
130 	return s == "null" || s == "NULL" || s == "nil" || s == "<nil>" || s == "None"
131 }
132 
133-func getFormattedValue(value string, colorize bool) string {
134-	if strings.Contains(value, " ") {
135-		value = fmt.Sprintf(`"%s"`, value)
136-	}
137-	if !colorize {
138-		return value
139-	}
140+func getFormattedValue(value string) string {
141 	if isNumeric(value) || isBoolean(value) {
142 		return color.MagentaString(value)
143 	}
144 	if isNull(value) {
145 		return color.YellowString(value)
146 	}
147+	if strings.Contains(value, " ") {
148+		value = fmt.Sprintf(`"%s"`, value)
149+	}
150 	return color.HiGreenString(value)
151 }
152 
153-func getFormattedLevel(level int, colorize bool) string {
154-	levelString := levelStrings[level]
155-	if !colorize {
156-		return levelString
157-	}
158-	return levelColors[level].Sprintf("[%s]", levelString)
159+func getFormattedLevel(level int) string {
160+	return levelColors[level].Sprintf("[%s]", levelStrings[level])
161 }
162 
163 func (r *Record) parseLevel(level string) {
164@@ -146,25 +139,21 @@ func (r *Record) MatchesFilter(filter map[string]string) bool {
165 	return true
166 }
167 
168-// String returns a formatted and optionally colorized string representation of the Record
169-func (r *Record) String(outputFields []string, colorize bool) string {
170+// String returns a formatted string representation of the Record
171+func (r *Record) String(outputFields []string) string {
172 	line := ""
173 	for _, key := range r.fieldOrder {
174 		if len(outputFields) > 0 && !slices.Contains(outputFields, key) {
175 			continue
176 		}
177 		value := r.fields[key]
178-		if colorize {
179-			key = color.HiBlueString(key)
180-		}
181-		line += fmt.Sprintf("%s=%s ", key, getFormattedValue(value, colorize))
182+		key = color.HiBlueString(key)
183+		line += fmt.Sprintf("%s=%s ", key, getFormattedValue(value))
184 	}
185-	var fmtString string
186-	if colorize {
187-		fmtString = "%s %26s %s"
188-	} else {
189-		fmtString = "%s %s %s"
190+	fmtString := "%s %26s %s"
191+	if color.NoColor {
192+		fmtString = "%s %7s %s"
193 	}
194 
195-	return fmt.Sprintf(fmtString, r.time.Format("2006-01-02 15:04:05"), getFormattedLevel(r.level, colorize), line)
196+	return fmt.Sprintf(fmtString, r.time.Format("2006-01-02 15:04:05"), getFormattedLevel(r.level), line)
197 }