From d20bbff6c26e1c6d74a9be1c94f67fa37c20a0d8 Mon Sep 17 00:00:00 2001 From: Conner Peirce Date: Sat, 6 May 2017 13:57:05 -0400 Subject: [PATCH 1/2] Support confining interpolated fields to a desired length - Very helpful if you're watching multiple fields that have a dynamic length and/or if you're interested in being able to view your logs in a truncated-columnar layout - Specifying a length after a field name reference is optional - It will truncate or pad to fit the string into the specified length e.g. $ elktail -f "%@timestamp[16] %msg[25] <- always the same length!" might print something like: 2017-05-06T17:47 ###.##.###.##:##### [06/May/2017:1 <- always the same length! 2017-05-06T17:47 ##.###.##.###:##### [06/May/2017:1 <- always the same length! 2017-05-06T17:47 ###.##.###.###:##### [06/May/2017: <- always the same length! 2017-05-06T17:47 ##.###.##.###:##### [06/May/2017:1 <- always the same length! 2017-05-06T17:47 ##.###.##.###:##### [06/May/2017:1 <- always the same length! 2017-05-06T17:47 ##.###.##.##:##### [06/May/2017:13 <- always the same length! 2017-05-06T17:47 ##.###.###.###:##### [06/May/2017: <- always the same length! 2017-05-06T17:47 ##.###.###.##:##### [06/May/2017:1 <- always the same length! 2017-05-06T17:47 ##.###.###.##:##### [06/May/2017:1 <- always the same length! 2017-05-06T17:47 ##.###.###.###:##### [06/May/2017: <- always the same length! --- configuration.go | 2 +- elktail.go | 58 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/configuration.go b/configuration.go index 573d8be..7bfe2be 100644 --- a/configuration.go +++ b/configuration.go @@ -156,7 +156,7 @@ func (config *Configuration) Flags() []cli.Flag { cli.StringFlag{ Name: "f,format", Value: "%message", - Usage: "(*) Message format for the entries - field names are referenced using % sign, for example '%@timestamp %message'", + Usage: "(*) Message format for the entries - field names are referenced using % sign, length with [n], for example '%@timestamp %message[55]'", Destination: &config.QueryDefinition.Format, }, cli.StringFlag{ diff --git a/elktail.go b/elktail.go index 69886e5..b5a7301 100644 --- a/elktail.go +++ b/elktail.go @@ -12,9 +12,13 @@ import ( "io/ioutil" "os" "regexp" + "strconv" "strings" "time" "golang.org/x/crypto/ssh/terminal" + + "unicode/utf8" + "github.com/codegangsta/cli" "net/url" "errors" @@ -41,8 +45,11 @@ func (entry *DisplayedEntry) isBefore(timeStamp string) bool { return entry.timeStamp < timeStamp } -// Regexp for parsing out format fields -var formatRegexp = regexp.MustCompile("%[A-Za-z0-9@_.-]+") +// FormatRegexp is used to parse out format fields from a user-supplied format string. +// Fields are prefaced with a '%' sign, and can be optionally suffixed with padding size +// in rune length (e.g. "%@timestamp %message[65]") +var FormatRegexp = regexp.MustCompile(`(%[A-Za-z0-9@_.-]+)(?:\[([1-9][0-9]*)\])?`) + const dateFormatDMY = "2006-01-02" const dateFormatFull = "2006-01-02T15:04:05.999Z07:00" const tailingTimeWindow = 500 @@ -255,23 +262,60 @@ func (t *Tail) processHit(hit *elastic.SearchHit) map[string]interface{} { Error.Fatalln("Failed parsing ElasticSearch response.", err) } t.printResult(entry) - return entry; + return entry } - // Print result according to format func (t *Tail) printResult(entry map[string]interface{}) { Trace.Println("Result: ", entry) - fields := formatRegexp.FindAllString(t.queryDefinition.Format, -1) + + fields := FormatRegexp.FindAllStringSubmatch(t.queryDefinition.Format, -1) + // e.g. #=> [["%message[24]", "%message", "24"]...] Trace.Println("Fields: ", fields) + result := t.queryDefinition.Format + for _, f := range fields { - value, _ := EvaluateExpression(entry, f[1:]) - result = strings.Replace(result, f, value, -1) + value, _ := EvaluateExpression(entry, f[1][1:]) + + if s := f[2]; s != "" { + colSize, _ := strconv.Atoi(f[2]) // regexp ensures parse-able + value = fitString(value, colSize) + } + result = strings.Replace(result, f[0], value, -1) } fmt.Println(result) } +// fitString strips runes from `s` (or pads with ' ') to +// create a string of length `n` +func fitString(s string, n int) string { + l := utf8.RuneCountInString(s) + + if l < n { + for p := 0; p < n-l; p++ { + s += " " + } + } else if l > n { + s = s[0:runeLengthToByteLength(s, n)] + } + + return s +} + +// runeLengthToByteLength returns the number of bytes in the first n +// runes of the string s +func runeLengthToByteLength(s string, n int) int { + b := 0 + for i := range s { + if i > n { + break + } + b = i + } + return b +} + func (t *Tail) buildSearchQuery() elastic.Query { var query elastic.Query if len(t.queryDefinition.Terms) > 0 { From 3135b0ff54137bff50e064662cd2246dae3ef240 Mon Sep 17 00:00:00 2001 From: Conner Peirce Date: Sun, 7 May 2017 18:25:01 -0400 Subject: [PATCH 2/2] Cover FormatRegexp for complete-ness --- elktail_test.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/elktail_test.go b/elktail_test.go index 8bce585..b0ed811 100644 --- a/elktail_test.go +++ b/elktail_test.go @@ -24,9 +24,29 @@ func TestResolveField(t *testing.T) { testutils.AssertEqualsString(t, "", eval(model1, "bar")) } - - func eval(model interface{}, expr string) string { result, _ := EvaluateExpression(model, expr) return result -} \ No newline at end of file +} + +func TestFormatRegexp(t *testing.T) { + formatString := "%timestamp %message[25] %trace[10] %error" + + match := FormatRegexp.FindAllStringSubmatch(formatString, -1) + + testutils.AssertEqualsString(t, "%timestamp", match[0][0]) + testutils.AssertEqualsString(t, "%timestamp", match[0][1]) + testutils.AssertEqualsString(t, "", match[0][2]) + + testutils.AssertEqualsString(t, "%message[25]", match[1][0]) + testutils.AssertEqualsString(t, "%message", match[1][1]) + testutils.AssertEqualsString(t, "25", match[1][2]) + + testutils.AssertEqualsString(t, "%trace[10]", match[2][0]) + testutils.AssertEqualsString(t, "%trace", match[2][1]) + testutils.AssertEqualsString(t, "10", match[2][2]) + + testutils.AssertEqualsString(t, "%error", match[3][0]) + testutils.AssertEqualsString(t, "%error", match[3][1]) + testutils.AssertEqualsString(t, "", match[3][2]) +}