├── .gitignore ├── .dockerignore ├── docs └── images │ ├── colors.png │ └── docker.png ├── Dockerfile ├── cmd └── tj │ ├── tokenBuffer.go │ ├── lines.go │ ├── json.go │ └── main.go ├── pkg └── color │ └── color.go ├── Makefile ├── README.md └── README.template.md /.gitignore: -------------------------------------------------------------------------------- 1 | binaries/ 2 | release/ -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !binaries/linux_x86_64/tj 3 | !cmd 4 | !pkg 5 | !Makefile -------------------------------------------------------------------------------- /docs/images/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgreben/tj/HEAD/docs/images/colors.png -------------------------------------------------------------------------------- /docs/images/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgreben/tj/HEAD/docs/images/docker.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.9.4-alpine3.7 2 | RUN apk add --no-cache make 3 | WORKDIR /go/src/github.com/sgreben/tj/ 4 | COPY . . 5 | RUN make binaries/linux_x86_64/tj 6 | 7 | FROM scratch 8 | COPY --from=0 /go/src/github.com/sgreben/tj/binaries/linux_x86_64/tj /tj 9 | ENTRYPOINT [ "/tj" ] -------------------------------------------------------------------------------- /cmd/tj/tokenBuffer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type tokenBuffer []tokenStream 4 | 5 | func (b *tokenBuffer) flush(token tokenStream) { 6 | for i, oldToken := range *b { 7 | oldToken.Token().copyDeltasFrom(token.Token()) 8 | print(oldToken) 9 | (*b)[i] = nil 10 | } 11 | *b = (*b)[:0] 12 | } 13 | -------------------------------------------------------------------------------- /cmd/tj/lines.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | ) 7 | 8 | type lineStream struct { 9 | token 10 | Text string `json:"text,omitempty"` 11 | 12 | buffer *tokenBuffer 13 | scanner *bufio.Scanner 14 | } 15 | 16 | func newLineStream() *lineStream { 17 | return &lineStream{ 18 | scanner: bufio.NewScanner(os.Stdin), 19 | buffer: &tokenBuffer{}, 20 | } 21 | } 22 | 23 | func (l *lineStream) Token() *token { 24 | return &l.token 25 | } 26 | 27 | func (l *lineStream) CopyCurrent() tokenStream { 28 | return &lineStream{ 29 | token: l.token, 30 | Text: l.Text, 31 | } 32 | } 33 | 34 | func (l *lineStream) AppendCurrentToBuffer() { 35 | *l.buffer = append(*l.buffer, l.CopyCurrent()) 36 | } 37 | 38 | func (l *lineStream) FlushBuffer() { 39 | l.buffer.flush(l) 40 | } 41 | 42 | func (l *lineStream) CurrentMatchText() string { 43 | if matchTemplate != nil { 44 | return matchTemplate.execute(l.Text) 45 | } 46 | return l.Text 47 | } 48 | 49 | func (l *lineStream) Err() error { 50 | return l.scanner.Err() 51 | } 52 | 53 | func (l *lineStream) Scan() bool { 54 | l.Text = "" 55 | ok := l.scanner.Scan() 56 | if ok { 57 | l.Text = l.scanner.Text() 58 | } 59 | return ok 60 | } 61 | -------------------------------------------------------------------------------- /cmd/tj/json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | const jsonStreamScratchBufferBytes = 4096 12 | 13 | type jsonStream struct { 14 | token 15 | Text string `json:"-"` // the original text that Object was parsed from 16 | Object interface{} `json:"object,omitempty"` 17 | 18 | textBuffer *bytes.Buffer // intercepts bytes read by decoder 19 | scratchBuffer []byte // determines size of decoder.Buffered() 20 | buffer *tokenBuffer 21 | decoder *json.Decoder 22 | decodeError error 23 | done bool 24 | } 25 | 26 | func newJSONStream() *jsonStream { 27 | textBuffer := bytes.NewBuffer(nil) 28 | tee := io.TeeReader(os.Stdin, textBuffer) 29 | return &jsonStream{ 30 | decoder: json.NewDecoder(tee), 31 | textBuffer: textBuffer, 32 | scratchBuffer: make([]byte, jsonStreamScratchBufferBytes), 33 | buffer: &tokenBuffer{}, 34 | } 35 | } 36 | 37 | func (j *jsonStream) Token() *token { 38 | return &j.token 39 | } 40 | 41 | func (j *jsonStream) CopyCurrent() tokenStream { 42 | return &jsonStream{ 43 | token: j.token, 44 | Object: j.Object, 45 | } 46 | } 47 | 48 | func (j *jsonStream) AppendCurrentToBuffer() { 49 | *j.buffer = append(*j.buffer, j.CopyCurrent()) 50 | } 51 | 52 | func (j *jsonStream) FlushBuffer() { 53 | j.buffer.flush(j) 54 | } 55 | 56 | func (j *jsonStream) CurrentMatchText() string { 57 | if matchTemplate != nil { 58 | return matchTemplate.execute(j.Object) 59 | } 60 | return j.Text 61 | } 62 | 63 | func (j *jsonStream) Err() error { 64 | if j.decodeError == io.EOF { 65 | return nil 66 | } 67 | return j.decodeError 68 | } 69 | 70 | func (j *jsonStream) readerSize(r io.Reader) int { 71 | total := 0 72 | var err error 73 | var n int 74 | for err == nil { 75 | n, err = r.Read(j.scratchBuffer) 76 | total += n 77 | } 78 | return total 79 | } 80 | 81 | func (j *jsonStream) Scan() bool { 82 | j.Object = new(interface{}) 83 | err := j.decoder.Decode(&j.Object) 84 | numBytesNotParsedByJSON := j.readerSize(j.decoder.Buffered()) // "{..} XYZ" -> len("XYZ") 85 | bytesUnreadByUs := j.textBuffer.Bytes() // "{..} XYZ" -> "{..} XYZ" 86 | numBytesUnreadByUs := len(bytesUnreadByUs) 87 | numBytesParsedByJSON := numBytesUnreadByUs - numBytesNotParsedByJSON // len("{..}") 88 | bytesReadByJSON := bytesUnreadByUs[:numBytesParsedByJSON] // "{..} XYZ" -> "{..}" 89 | j.Text = strings.TrimSpace(string(bytesReadByJSON)) 90 | j.textBuffer.Next(numBytesParsedByJSON) // "*{..} XYZ" -> "*XYZ" 91 | if err != nil { 92 | if j.decodeError == nil || j.decodeError == io.EOF { 93 | j.decodeError = err 94 | } 95 | return false 96 | } 97 | return true 98 | } 99 | -------------------------------------------------------------------------------- /pkg/color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type rgb struct{ r, g, b uint8 } 12 | 13 | // Scale is a color scale, a function mapping [0,1] to rgb colors. 14 | type Scale func(float64) (r, g, b uint8) 15 | 16 | func index(r, g, b uint8) int { 17 | ri := (int(r) * 5) / 0xFF 18 | gi := (int(g) * 5) / 0xFF 19 | bi := (int(b) * 5) / 0xFF 20 | return 36*ri + 6*gi + bi + 16 21 | } 22 | 23 | func Cube(scale Scale) Scale { 24 | return func(c float64) (r, g, b uint8) { 25 | c = clamp(c) 26 | return scale(c * c * c) 27 | } 28 | } 29 | 30 | func Sqr(scale Scale) Scale { 31 | return func(c float64) (r, g, b uint8) { 32 | c = clamp(c) 33 | return scale(c * c) 34 | } 35 | } 36 | 37 | func Sqrt(scale Scale) Scale { 38 | return func(c float64) (r, g, b uint8) { 39 | c = clamp(c) 40 | return scale(math.Sqrt(c)) 41 | } 42 | } 43 | 44 | func Cubert(scale Scale) Scale { 45 | return func(c float64) (r, g, b uint8) { 46 | c = clamp(c) 47 | return scale(math.Pow(c, 1.0/3.0)) 48 | } 49 | } 50 | 51 | func clamp(c float64) float64 { 52 | if c < 0 { 53 | c = 0 54 | } 55 | if c > 1 { 56 | c = 1 57 | } 58 | return c 59 | } 60 | 61 | var notHexChars = regexp.MustCompile("[^0-9a-fA-F]+") 62 | 63 | func parse3(s string, c *rgb) { 64 | r, _ := strconv.ParseUint(s[0:1], 16, 8) 65 | c.r = uint8((r << 4) | r) 66 | g, _ := strconv.ParseUint(s[1:2], 16, 8) 67 | c.g = uint8((g << 4) | g) 68 | b, _ := strconv.ParseUint(s[2:3], 16, 8) 69 | c.b = uint8((b << 4) | b) 70 | } 71 | 72 | func parse6(s string, c *rgb) { 73 | r, _ := strconv.ParseUint(s[0:2], 16, 8) 74 | c.r = uint8(r) 75 | g, _ := strconv.ParseUint(s[2:4], 16, 8) 76 | c.g = uint8(g) 77 | b, _ := strconv.ParseUint(s[4:6], 16, 8) 78 | c.b = uint8(b) 79 | } 80 | 81 | // ParseScale parses a sequence of hex colors as a Scale 82 | func ParseScale(scale string) Scale { 83 | hexOnly := notHexChars.ReplaceAllString(scale, " ") 84 | trimmed := strings.TrimSpace(hexOnly) 85 | lowercase := strings.ToLower(trimmed) 86 | parts := strings.Split(lowercase, " ") 87 | 88 | colors := make([]rgb, len(parts)) 89 | for i, s := range parts { 90 | switch len(s) { 91 | case 3: 92 | parse3(s, &colors[i]) 93 | case 6: 94 | parse6(s, &colors[i]) 95 | } 96 | } 97 | return func(c float64) (r, g, b uint8) { 98 | return interpolate(c, colors) 99 | } 100 | } 101 | 102 | func interpolate2(c float64, r1, g1, b1, r2, g2, b2 uint8) (r, g, b uint8) { 103 | r = uint8(float64(r1)*(1-c) + float64(r2)*c) 104 | g = uint8(float64(g1)*(1-c) + float64(g2)*c) 105 | b = uint8(float64(b1)*(1-c) + float64(b2)*c) 106 | return 107 | } 108 | 109 | func interpolate(c float64, points []rgb) (r, g, b uint8) { 110 | c = clamp(c) 111 | x := float64(len(points)-1) * c 112 | i := int(x) 113 | left := points[i] 114 | j := int(x + 1) 115 | if j >= len(points) { 116 | j = i 117 | } 118 | right := points[j] 119 | c = x - float64(i) 120 | return interpolate2(c, left.r, left.g, left.b, right.r, right.g, right.b) 121 | } 122 | 123 | // Foreground returns the closest matching terminal foreground color escape sequence 124 | func Foreground(r, g, b uint8) string { 125 | return fmt.Sprintf("\033[38;5;%dm", index(r, g, b)) 126 | } 127 | 128 | // Background returns the closest matching terminal background color escape sequence 129 | func Background(r, g, b uint8) string { 130 | return fmt.Sprintf("\033[48;5%dm", index(r, g, b)) 131 | } 132 | 133 | // Reset is the color reset terminal escape sequence 134 | const Reset = "\033[0;00m" 135 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 7.0.0 2 | 3 | APP := tj 4 | PACKAGES := $(shell go list -f {{.Dir}} ./...) 5 | GOFILES := $(addsuffix /*.go,$(PACKAGES)) 6 | GOFILES := $(wildcard $(GOFILES)) 7 | 8 | .PHONY: clean release release-ci release-manual docker docker-latest README.md 9 | 10 | clean: 11 | rm -rf binaries/ 12 | rm -rf release/ 13 | 14 | release-ci: README.md zip 15 | 16 | release: README.md 17 | git reset 18 | git add README.md 19 | git add Makefile 20 | git commit -am "Release $(VERSION)" || true 21 | git tag "$(VERSION)" 22 | git push 23 | git push origin "$(VERSION)" 24 | 25 | # go get -u github.com/github/hub 26 | release-manual: README.md zip 27 | git push 28 | hub release create $(VERSION) -m "$(VERSION)" -a release/$(APP)_$(VERSION)_osx_x86_64.tar.gz -a release/$(APP)_$(VERSION)_windows_x86_64.zip -a release/$(APP)_$(VERSION)_linux_x86_64.tar.gz -a release/$(APP)_$(VERSION)_osx_x86_32.tar.gz -a release/$(APP)_$(VERSION)_windows_x86_32.zip -a release/$(APP)_$(VERSION)_linux_x86_32.tar.gz -a release/$(APP)_$(VERSION)_linux_arm64.tar.gz 29 | 30 | README.md: 31 | sed "s/\$${VERSION}/$(VERSION)/g;s/\$${APP}/$(APP)/g;" README.template.md > README.md 32 | 33 | docker: binaries/linux_x86_64/$(APP) 34 | docker build -t quay.io/sergey_grebenshchikov/$(APP):v$(VERSION) . 35 | docker push quay.io/sergey_grebenshchikov/$(APP):v$(VERSION) 36 | 37 | docker-latest: docker 38 | docker tag quay.io/sergey_grebenshchikov/$(APP):v$(VERSION) quay.io/sergey_grebenshchikov/$(APP):latest 39 | docker push quay.io/sergey_grebenshchikov/$(APP):latest 40 | 41 | zip: release/$(APP)_$(VERSION)_osx_x86_64.tar.gz release/$(APP)_$(VERSION)_windows_x86_64.zip release/$(APP)_$(VERSION)_linux_x86_64.tar.gz release/$(APP)_$(VERSION)_osx_x86_32.tar.gz release/$(APP)_$(VERSION)_windows_x86_32.zip release/$(APP)_$(VERSION)_linux_x86_32.tar.gz release/$(APP)_$(VERSION)_linux_arm64.tar.gz 42 | 43 | binaries: binaries/osx_x86_64/$(APP) binaries/windows_x86_64/$(APP).exe binaries/linux_x86_64/$(APP) binaries/osx_x86_32/$(APP) binaries/windows_x86_32/$(APP).exe binaries/linux_x86_32/$(APP) 44 | 45 | release/$(APP)_$(VERSION)_osx_x86_64.tar.gz: binaries/osx_x86_64/$(APP) 46 | mkdir -p release 47 | tar cfz release/$(APP)_$(VERSION)_osx_x86_64.tar.gz -C binaries/osx_x86_64 $(APP) 48 | 49 | binaries/osx_x86_64/$(APP): $(GOFILES) 50 | GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/osx_x86_64/$(APP) ./cmd/$(APP) 51 | 52 | release/$(APP)_$(VERSION)_windows_x86_64.zip: binaries/windows_x86_64/$(APP).exe 53 | mkdir -p release 54 | cd ./binaries/windows_x86_64 && zip -r -D ../../release/$(APP)_$(VERSION)_windows_x86_64.zip $(APP).exe 55 | 56 | binaries/windows_x86_64/$(APP).exe: $(GOFILES) 57 | GOOS=windows GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/windows_x86_64/$(APP).exe ./cmd/$(APP) 58 | 59 | release/$(APP)_$(VERSION)_linux_x86_64.tar.gz: binaries/linux_x86_64/$(APP) 60 | mkdir -p release 61 | tar cfz release/$(APP)_$(VERSION)_linux_x86_64.tar.gz -C binaries/linux_x86_64 $(APP) 62 | 63 | binaries/linux_x86_64/$(APP): $(GOFILES) 64 | GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_x86_64/$(APP) ./cmd/$(APP) 65 | 66 | release/$(APP)_$(VERSION)_osx_x86_32.tar.gz: binaries/osx_x86_32/$(APP) 67 | mkdir -p release 68 | tar cfz release/$(APP)_$(VERSION)_osx_x86_32.tar.gz -C binaries/osx_x86_32 $(APP) 69 | 70 | binaries/osx_x86_32/$(APP): $(GOFILES) 71 | GOOS=darwin GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/osx_x86_32/$(APP) ./cmd/$(APP) 72 | 73 | release/$(APP)_$(VERSION)_windows_x86_32.zip: binaries/windows_x86_32/$(APP).exe 74 | mkdir -p release 75 | cd ./binaries/windows_x86_32 && zip -r -D ../../release/$(APP)_$(VERSION)_windows_x86_32.zip $(APP).exe 76 | 77 | binaries/windows_x86_32/$(APP).exe: $(GOFILES) 78 | GOOS=windows GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/windows_x86_32/$(APP).exe ./cmd/$(APP) 79 | 80 | release/$(APP)_$(VERSION)_linux_x86_32.tar.gz: binaries/linux_x86_32/$(APP) 81 | mkdir -p release 82 | tar cfz release/$(APP)_$(VERSION)_linux_x86_32.tar.gz -C binaries/linux_x86_32 $(APP) 83 | 84 | binaries/linux_x86_32/$(APP): $(GOFILES) 85 | GOOS=linux GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_x86_32/$(APP) ./cmd/$(APP) 86 | 87 | release/$(APP)_$(VERSION)_linux_arm64.tar.gz: binaries/linux_arm64/$(APP) 88 | mkdir -p release 89 | tar cfz release/$(APP)_$(VERSION)_linux_arm64.tar.gz -C binaries/linux_arm64 $(APP) 90 | 91 | binaries/linux_arm64/$(APP): $(GOFILES) 92 | GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_arm64/$(APP) ./cmd/$(APP) 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tj - stdin line timestamps, JSON-friendly 2 | 3 | `tj` timestamps lines read from standard input. 4 | 5 | - [Get it](#get-it) 6 | - [Use it](#use-it) 7 | - [JSON output](#json-output) 8 | - [Time format](#time-format) 9 | - [Template output](#template-output) 10 | - [Color output](#color-output) 11 | - [JSON input](#json-input) 12 | - [Stopwatch regex](#stopwatch-regex) 13 | - [Stopwatch regex template](#stopwatch-regex-template) 14 | - [Stopwatch condition](#stopwatch-condition) 15 | - [Example](#example) 16 | - [Comments](https://github.com/sgreben/tj/issues/1) 17 | 18 | 19 | ## Get it 20 | 21 | Using go get: 22 | 23 | ```bash 24 | go get -u github.com/sgreben/tj/cmd/tj 25 | ``` 26 | 27 | Or [download the binary](https://github.com/sgreben/tj/releases/latest) from the releases page: 28 | 29 | ```bash 30 | # Linux 31 | curl -L https://github.com/sgreben/tj/releases/download/7.0.0/tj_7.0.0_linux_x86_64.tar.gz | tar xz 32 | 33 | # OS X 34 | curl -L https://github.com/sgreben/tj/releases/download/7.0.0/tj_7.0.0_osx_x86_64.tar.gz | tar xz 35 | 36 | # Windows 37 | curl -LO https://github.com/sgreben/tj/releases/download/7.0.0/tj_7.0.0_windows_x86_64.zip 38 | unzip tj_7.0.0_windows_x86_64.zip 39 | ``` 40 | 41 | Also available as a [docker image](https://quay.io/repository/sergey_grebenshchikov/tj?tab=tags): 42 | 43 | ```bash 44 | docker pull quay.io/sergey_grebenshchikov/tj 45 | ``` 46 | 47 | ## Use it 48 | 49 | `tj` reads from stdin and writes to stdout. 50 | 51 | ```text 52 | Usage of tj: 53 | -template string 54 | either a go template (https://golang.org/pkg/text/template) or one of the predefined template names 55 | -time-format string 56 | either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants) 57 | -time-zone string 58 | time zone to use (or "Local") (default "UTC") 59 | -match-regex string 60 | a regex pattern. if given, only tokens matching it (re)start the stopwatch 61 | -match-template string 62 | go template, used to extract text used for -match-regex 63 | -match-condition string 64 | go template. if given, only tokens that result in 'true' (re)start the stopwatch 65 | -match-buffer 66 | buffer lines between matches of -match-regex / -match-condition, copy delta values from final line to buffered lines 67 | -match string 68 | alias for -match-template 69 | -condition string 70 | alias for -match-condition 71 | -regex string 72 | alias for -match-regex 73 | -read-json 74 | parse a sequence of JSON objects from stdin 75 | -scale string 76 | either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow) 77 | (default "BlueToRed") 78 | -scale-fast duration 79 | the lower bound for the color scale (default 100ms) 80 | -scale-slow duration 81 | the upper bound for the color scale (default 2s) 82 | -scale-linear 83 | use linear scale (default true) 84 | -scale-cube 85 | use cubic scale 86 | -scale-cubert 87 | use cubic root scale 88 | -scale-sqr 89 | use quadratic scale 90 | -scale-sqrt 91 | use quadratic root scale 92 | -version 93 | print version and exit 94 | ``` 95 | 96 | ### JSON output 97 | 98 | The default output format is JSON, one object per line: 99 | 100 | ```bash 101 | $ (echo Hello; echo World) | tj 102 | ``` 103 | 104 | ```json 105 | {"timeSecs":1517592179,"timeNanos":1517592179895262811,"time":"2018-02-02T18:22:59+01:00","deltaSecs":0.000016485,"deltaNanos":16485,"delta":"16.485µs","totalSecs":0.000016485,"totalNanos":16485,"total":"16.485µs","text":"Hello"} 106 | {"timeSecs":1517592179,"timeNanos":1517592179895451948,"time":"2018-02-02T18:22:59+01:00","deltaSecs":0.000189137,"deltaNanos":189137,"delta":"189.137µs","totalSecs":0.000205622,"totalNanos":205622,"total":"205.622µs","text":"World"} 107 | ``` 108 | 109 | ### Time format 110 | 111 | You can set the format of the `time` field using the `-time-format` parameter: 112 | 113 | ```bash 114 | $ (echo Hello; echo World) | tj -time-format Kitchen 115 | ``` 116 | 117 | ```json 118 | {"timeSecs":1517592194,"timeNanos":1517592194875016639,"time":"6:23PM","deltaSecs":0.000017142,"deltaNanos":17142,"delta":"17.142µs","totalSecs":0.000017142,"totalNanos":17142,"total":"17.142µs","text":"Hello"} 119 | {"timeSecs":1517592194,"timeNanos":1517592194875197515,"time":"6:23PM","deltaSecs":0.000180876,"deltaNanos":180876,"delta":"180.876µs","totalSecs":0.000198018,"totalNanos":198018,"total":"198.018µs","text":"World"} 120 | ``` 121 | 122 | The [constant names from pkg/time](https://golang.org/pkg/time/#pkg-constants) as well as regular go time layouts are valid values for `-time-format`: 123 | 124 | | Name | Format | 125 | |------------|-------------------------------------| 126 | | ANSIC | `Mon Jan _2 15:04:05 2006` | 127 | | Kitchen | `3:04PM` | 128 | | ISO8601 | `2006-01-02T15:04:05Z07:00` | 129 | | RFC1123 | `Mon, 02 Jan 2006 15:04:05 MST` | 130 | | RFC1123Z | `Mon, 02 Jan 2006 15:04:05 -0700` | 131 | | RFC3339 | `2006-01-02T15:04:05Z07:00` | 132 | | RFC3339Nano| `2006-01-02T15:04:05.999999999Z07:00` 133 | | RFC822 | `02 Jan 06 15:04 MST` | 134 | | RFC822Z | `02 Jan 06 15:04 -0700` | 135 | | RFC850 | `Monday, 02-Jan-06 15:04:05 MST` | 136 | | RubyDate | `Mon Jan 02 15:04:05 -0700 2006` | 137 | | Stamp | `Jan _2 15:04:05` | 138 | | StampMicro | `Jan _2 15:04:05.000000` | 139 | | StampMilli | `Jan _2 15:04:05.000` | 140 | | StampNano | `Jan _2 15:04:05.000000000` | 141 | | UnixDate | `Mon Jan _2 15:04:05 MST 2006` | 142 | 143 | ### Template output 144 | 145 | You can also specify an output template using the `-template` parameter and [go template](https://golang.org/pkg/text/template) syntax: 146 | 147 | ```bash 148 | $ (echo Hello; echo World) | tj -template '{{ .I }} {{.TimeSecs}} {{.Text}}' 149 | ``` 150 | 151 | ```json 152 | 0 1516649679 Hello 153 | 1 1516649679 World 154 | ``` 155 | 156 | The fields available to the template are specified in the [`token` struct](cmd/tj/main.go#L18). 157 | 158 | Some templates are pre-defined and can be used via `-template NAME`: 159 | 160 | | Name | Template | 161 | |-----------------|--------------------------------------------------| 162 | | Color | `{{color .}}█{{reset}} {{.Text}}` | 163 | | ColorText | `{{color .}}{{.Text}}{{reset}}` | 164 | | Delta | `{{.Delta}} {{.Text}}` | 165 | | DeltaColor | `{{.Delta}} {{color .}}█{{reset}} {{.Text}}` | 166 | | DeltaNanos | `{{.DeltaNanos}} {{.Text}}` | 167 | | Text | `{{.Text}}` | 168 | | Time | `{{.TimeString}} {{.Text}}` | 169 | | TimeColor | `{{.TimeString}} {{color .}}█{{reset}} {{.Text}}`| 170 | | TimeDelta | `{{.TimeString}} +{{.Delta}} {{.Text}}` | 171 | | TimeDeltaNanos | `{{.TimeString}} +{{.DeltaNanos}} {{.Text}}` | 172 | 173 | ### Color output 174 | 175 | To help identify durations at a glance, `tj` maps durations to a color scale. The pre-defined templates `Color` and `ColorText` demonstrate this: 176 | 177 | ```bash 178 | $ (echo fast; 179 | sleep 1; 180 | echo slower; 181 | sleep 1.5; 182 | echo slow; 183 | sleep 2; 184 | echo slowest) | tj -template Color 185 | ``` 186 | ![Color output](docs/images/colors.png) 187 | 188 | The terminal foreground color can be set by using `{{color .}}` in the output template. The default terminal color can be restored using `{{reset}}`. 189 | 190 | The color scale can be set using the parameters `-scale`, `-scale-fast`, and `-scale-slow`: 191 | 192 | - The `-scale` parameter defines the colors used in the scale. 193 | - The `-scale-fast` and `-scale-slow` parameters define the boundaries of the scale: durations shorter than the value of `-scale-fast` are mapped to the leftmost color, durations longer than the value of `-scale-slow` are mapped to the rightmost color. 194 | 195 | The scale is linear by default, but can be transformed: 196 | 197 | - `-scale-sqr`, `-scale-sqrt` yields a quadratic (root) scale 198 | - `-scale-cube`, `-scale-cubert` yields a cubic (root) scale 199 | 200 | There are several pre-defined color scales: 201 | 202 | | Name | Scale | 203 | |---------------------|----------------------- | 204 | | BlackToPurple | `#000 -> #F700FF` | 205 | | BlackToRed | `#000 -> #F00` | 206 | | BlueToRed | `#00F -> #F00` | 207 | | CyanToRed | `#0FF -> #F00` | 208 | | GreenToRed | `#0F0 -> #F00` | 209 | | GreenToGreenToRed | `#0F0 -> #0F0 -> #F00` | 210 | | WhiteToPurple | `#FFF -> #F700FF` | 211 | | WhiteToRed | `#FFF -> #F00` | 212 | | WhiteToBlueToRed | `#FFF -> #00F -> #F00` | 213 | 214 | You can also provide your own color scale using the same syntax as the pre-defined ones. 215 | 216 | ### JSON input 217 | 218 | Using `-read-json`, you can tell `tj` to parse stdin as a sequence of JSON objects. The parsed object can be referred to via `.Object`, like this: 219 | 220 | ```bash 221 | $ echo '{"hello": "World"}' | tj -read-json -template "{{.TimeString}} {{.Object.hello}}" 222 | ``` 223 | 224 | ``` 225 | 2018-01-25T21:55:06+01:00 World 226 | ``` 227 | 228 | The exact JSON string that was parsed can be recovered using `.Text`: 229 | 230 | ```bash 231 | $ echo '{"hello" : "World"} { }' | tj -read-json -template "{{.TimeString}} {{.Text}}" 232 | ``` 233 | 234 | ``` 235 | 2018-01-25T21:55:06+01:00 {"hello" : "World"} 236 | 2018-01-25T21:55:06+01:00 { } 237 | ``` 238 | 239 | ### Stopwatch regex 240 | 241 | Sometimes you need to measure the duration between certain *tokens* in the input. 242 | 243 | To help with this, `tj` can match each line against a regular expression and only reset the stopwatch (`delta`, `deltaSecs`, `deltaNanos`) when a line matches. The regular expression can be specified via the `-match-regex` (alias `-regex`) parameter. 244 | 245 | ### Stopwatch regex template 246 | 247 | When using `-match-regex`, you can also specify a template `-match-template` (alias `-match`) to extract text from the current token. The output of this template is matched against the stopwatch regex. 248 | 249 | This allows you to use only specific fields of JSON objects as stopwatch reset triggers. For example: 250 | 251 | ```bash 252 | $ (echo {}; sleep 1; echo {}; sleep 1; echo '{"reset": "yes"}'; echo {}) | 253 | tj -read-json -match .reset -regex yes -template "{{.I}} {{.DeltaNanos}}" 254 | ``` 255 | 256 | ``` 257 | 0 14374 258 | 1 1005916918 259 | 2 2017292187 260 | 3 79099 261 | ``` 262 | 263 | The output of the match template is stored in the field `.MatchText` of the `token` struct: 264 | 265 | ```bash 266 | $ echo '{"message":"hello"}' | tj -read-json -match-template .message -template "{{.TimeString}} {{.MatchText}}" 267 | ``` 268 | 269 | ``` 270 | 2018-01-25T22:20:59+01:00 hello 271 | ``` 272 | 273 | ### Stopwatch condition 274 | 275 | Additionally to `-match-regex`, you can specify a `-match-condition` go template. If this template produces the literal string `true`, the stopwatch is reset - "matches" of the `-match-condition` are treated like matches of the `-match-regex`. 276 | 277 | ## Example 278 | 279 | Finding the slowest step in a `docker build` (using `jq`): 280 | 281 | ```bash 282 | $ cat Dockerfile 283 | FROM alpine 284 | RUN echo About to be slow... 285 | RUN sleep 10 286 | RUN echo Done being slow 287 | ``` 288 | 289 | ```bash 290 | $ docker build . | 291 | tj -regex ^Step | 292 | jq -s 'max_by(.deltaNanos) | {step:.start.text, duration:.delta}' 293 | ``` 294 | 295 | ```json 296 | {"step":"Step 3/4 : RUN sleep 10","duration":"10.602026127s"} 297 | ``` 298 | 299 | Alternatively, using color output and buffering: 300 | 301 | ```bash 302 | $ docker build . | 303 | tj -regex ^Step -match-buffer -template Color -scale-cube 304 | ``` 305 | 306 | ![Docker build with color output](docs/images/docker.png) 307 | 308 | ## Comments 309 | 310 | Feel free to [leave a comment](https://github.com/sgreben/tj/issues/1) or create an issue. 311 | -------------------------------------------------------------------------------- /README.template.md: -------------------------------------------------------------------------------- 1 | # tj - stdin line timestamps, JSON-friendly 2 | 3 | `tj` timestamps lines read from standard input. 4 | 5 | - [Get it](#get-it) 6 | - [Use it](#use-it) 7 | - [JSON output](#json-output) 8 | - [Time format](#time-format) 9 | - [Template output](#template-output) 10 | - [Color output](#color-output) 11 | - [JSON input](#json-input) 12 | - [Stopwatch regex](#stopwatch-regex) 13 | - [Stopwatch regex template](#stopwatch-regex-template) 14 | - [Stopwatch condition](#stopwatch-condition) 15 | - [Example](#example) 16 | - [Comments](https://github.com/sgreben/tj/issues/1) 17 | 18 | 19 | ## Get it 20 | 21 | Using go get: 22 | 23 | ```bash 24 | go get -u github.com/sgreben/tj/cmd/tj 25 | ``` 26 | 27 | Or [download the binary](https://github.com/sgreben/tj/releases/latest) from the releases page: 28 | 29 | ```bash 30 | # Linux 31 | curl -L https://github.com/sgreben/tj/releases/download/${VERSION}/tj_${VERSION}_linux_x86_64.tar.gz | tar xz 32 | 33 | # OS X 34 | curl -L https://github.com/sgreben/tj/releases/download/${VERSION}/tj_${VERSION}_osx_x86_64.tar.gz | tar xz 35 | 36 | # Windows 37 | curl -LO https://github.com/sgreben/tj/releases/download/${VERSION}/tj_${VERSION}_windows_x86_64.zip 38 | unzip tj_${VERSION}_windows_x86_64.zip 39 | ``` 40 | 41 | Also available as a [docker image](https://quay.io/repository/sergey_grebenshchikov/tj?tab=tags): 42 | 43 | ```bash 44 | docker pull quay.io/sergey_grebenshchikov/tj 45 | ``` 46 | 47 | ## Use it 48 | 49 | `tj` reads from stdin and writes to stdout. 50 | 51 | ```text 52 | Usage of tj: 53 | -template string 54 | either a go template (https://golang.org/pkg/text/template) or one of the predefined template names 55 | -time-format string 56 | either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants) 57 | -time-zone string 58 | time zone to use (or "Local") (default "UTC") 59 | -match-regex string 60 | a regex pattern. if given, only tokens matching it (re)start the stopwatch 61 | -match-template string 62 | go template, used to extract text used for -match-regex 63 | -match-condition string 64 | go template. if given, only tokens that result in 'true' (re)start the stopwatch 65 | -match-buffer 66 | buffer lines between matches of -match-regex / -match-condition, copy delta values from final line to buffered lines 67 | -match string 68 | alias for -match-template 69 | -condition string 70 | alias for -match-condition 71 | -regex string 72 | alias for -match-regex 73 | -read-json 74 | parse a sequence of JSON objects from stdin 75 | -scale string 76 | either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow) 77 | (default "BlueToRed") 78 | -scale-fast duration 79 | the lower bound for the color scale (default 100ms) 80 | -scale-slow duration 81 | the upper bound for the color scale (default 2s) 82 | -scale-linear 83 | use linear scale (default true) 84 | -scale-cube 85 | use cubic scale 86 | -scale-cubert 87 | use cubic root scale 88 | -scale-sqr 89 | use quadratic scale 90 | -scale-sqrt 91 | use quadratic root scale 92 | -version 93 | print version and exit 94 | ``` 95 | 96 | ### JSON output 97 | 98 | The default output format is JSON, one object per line: 99 | 100 | ```bash 101 | $ (echo Hello; echo World) | tj 102 | ``` 103 | 104 | ```json 105 | {"timeSecs":1517592179,"timeNanos":1517592179895262811,"time":"2018-02-02T18:22:59+01:00","deltaSecs":0.000016485,"deltaNanos":16485,"delta":"16.485µs","totalSecs":0.000016485,"totalNanos":16485,"total":"16.485µs","text":"Hello"} 106 | {"timeSecs":1517592179,"timeNanos":1517592179895451948,"time":"2018-02-02T18:22:59+01:00","deltaSecs":0.000189137,"deltaNanos":189137,"delta":"189.137µs","totalSecs":0.000205622,"totalNanos":205622,"total":"205.622µs","text":"World"} 107 | ``` 108 | 109 | ### Time format 110 | 111 | You can set the format of the `time` field using the `-time-format` parameter: 112 | 113 | ```bash 114 | $ (echo Hello; echo World) | tj -time-format Kitchen 115 | ``` 116 | 117 | ```json 118 | {"timeSecs":1517592194,"timeNanos":1517592194875016639,"time":"6:23PM","deltaSecs":0.000017142,"deltaNanos":17142,"delta":"17.142µs","totalSecs":0.000017142,"totalNanos":17142,"total":"17.142µs","text":"Hello"} 119 | {"timeSecs":1517592194,"timeNanos":1517592194875197515,"time":"6:23PM","deltaSecs":0.000180876,"deltaNanos":180876,"delta":"180.876µs","totalSecs":0.000198018,"totalNanos":198018,"total":"198.018µs","text":"World"} 120 | ``` 121 | 122 | The [constant names from pkg/time](https://golang.org/pkg/time/#pkg-constants) as well as regular go time layouts are valid values for `-time-format`: 123 | 124 | | Name | Format | 125 | |------------|-------------------------------------| 126 | | ANSIC | `Mon Jan _2 15:04:05 2006` | 127 | | Kitchen | `3:04PM` | 128 | | ISO8601 | `2006-01-02T15:04:05Z07:00` | 129 | | RFC1123 | `Mon, 02 Jan 2006 15:04:05 MST` | 130 | | RFC1123Z | `Mon, 02 Jan 2006 15:04:05 -0700` | 131 | | RFC3339 | `2006-01-02T15:04:05Z07:00` | 132 | | RFC3339Nano| `2006-01-02T15:04:05.999999999Z07:00` 133 | | RFC822 | `02 Jan 06 15:04 MST` | 134 | | RFC822Z | `02 Jan 06 15:04 -0700` | 135 | | RFC850 | `Monday, 02-Jan-06 15:04:05 MST` | 136 | | RubyDate | `Mon Jan 02 15:04:05 -0700 2006` | 137 | | Stamp | `Jan _2 15:04:05` | 138 | | StampMicro | `Jan _2 15:04:05.000000` | 139 | | StampMilli | `Jan _2 15:04:05.000` | 140 | | StampNano | `Jan _2 15:04:05.000000000` | 141 | | UnixDate | `Mon Jan _2 15:04:05 MST 2006` | 142 | 143 | ### Template output 144 | 145 | You can also specify an output template using the `-template` parameter and [go template](https://golang.org/pkg/text/template) syntax: 146 | 147 | ```bash 148 | $ (echo Hello; echo World) | tj -template '{{ .I }} {{.TimeSecs}} {{.Text}}' 149 | ``` 150 | 151 | ```json 152 | 0 1516649679 Hello 153 | 1 1516649679 World 154 | ``` 155 | 156 | The fields available to the template are specified in the [`token` struct](cmd/tj/main.go#L18). 157 | 158 | Some templates are pre-defined and can be used via `-template NAME`: 159 | 160 | | Name | Template | 161 | |-----------------|--------------------------------------------------| 162 | | Color | `{{color .}}█{{reset}} {{.Text}}` | 163 | | ColorText | `{{color .}}{{.Text}}{{reset}}` | 164 | | Delta | `{{.Delta}} {{.Text}}` | 165 | | DeltaColor | `{{.Delta}} {{color .}}█{{reset}} {{.Text}}` | 166 | | DeltaNanos | `{{.DeltaNanos}} {{.Text}}` | 167 | | Text | `{{.Text}}` | 168 | | Time | `{{.TimeString}} {{.Text}}` | 169 | | TimeColor | `{{.TimeString}} {{color .}}█{{reset}} {{.Text}}`| 170 | | TimeDelta | `{{.TimeString}} +{{.Delta}} {{.Text}}` | 171 | | TimeDeltaNanos | `{{.TimeString}} +{{.DeltaNanos}} {{.Text}}` | 172 | 173 | ### Color output 174 | 175 | To help identify durations at a glance, `tj` maps durations to a color scale. The pre-defined templates `Color` and `ColorText` demonstrate this: 176 | 177 | ```bash 178 | $ (echo fast; 179 | sleep 1; 180 | echo slower; 181 | sleep 1.5; 182 | echo slow; 183 | sleep 2; 184 | echo slowest) | tj -template Color 185 | ``` 186 | ![Color output](docs/images/colors.png) 187 | 188 | The terminal foreground color can be set by using `{{color .}}` in the output template. The default terminal color can be restored using `{{reset}}`. 189 | 190 | The color scale can be set using the parameters `-scale`, `-scale-fast`, and `-scale-slow`: 191 | 192 | - The `-scale` parameter defines the colors used in the scale. 193 | - The `-scale-fast` and `-scale-slow` parameters define the boundaries of the scale: durations shorter than the value of `-scale-fast` are mapped to the leftmost color, durations longer than the value of `-scale-slow` are mapped to the rightmost color. 194 | 195 | The scale is linear by default, but can be transformed: 196 | 197 | - `-scale-sqr`, `-scale-sqrt` yields a quadratic (root) scale 198 | - `-scale-cube`, `-scale-cubert` yields a cubic (root) scale 199 | 200 | There are several pre-defined color scales: 201 | 202 | | Name | Scale | 203 | |---------------------|----------------------- | 204 | | BlackToPurple | `#000 -> #F700FF` | 205 | | BlackToRed | `#000 -> #F00` | 206 | | BlueToRed | `#00F -> #F00` | 207 | | CyanToRed | `#0FF -> #F00` | 208 | | GreenToRed | `#0F0 -> #F00` | 209 | | GreenToGreenToRed | `#0F0 -> #0F0 -> #F00` | 210 | | WhiteToPurple | `#FFF -> #F700FF` | 211 | | WhiteToRed | `#FFF -> #F00` | 212 | | WhiteToBlueToRed | `#FFF -> #00F -> #F00` | 213 | 214 | You can also provide your own color scale using the same syntax as the pre-defined ones. 215 | 216 | ### JSON input 217 | 218 | Using `-read-json`, you can tell `tj` to parse stdin as a sequence of JSON objects. The parsed object can be referred to via `.Object`, like this: 219 | 220 | ```bash 221 | $ echo '{"hello": "World"}' | tj -read-json -template "{{.TimeString}} {{.Object.hello}}" 222 | ``` 223 | 224 | ``` 225 | 2018-01-25T21:55:06+01:00 World 226 | ``` 227 | 228 | The exact JSON string that was parsed can be recovered using `.Text`: 229 | 230 | ```bash 231 | $ echo '{"hello" : "World"} { }' | tj -read-json -template "{{.TimeString}} {{.Text}}" 232 | ``` 233 | 234 | ``` 235 | 2018-01-25T21:55:06+01:00 {"hello" : "World"} 236 | 2018-01-25T21:55:06+01:00 { } 237 | ``` 238 | 239 | ### Stopwatch regex 240 | 241 | Sometimes you need to measure the duration between certain *tokens* in the input. 242 | 243 | To help with this, `tj` can match each line against a regular expression and only reset the stopwatch (`delta`, `deltaSecs`, `deltaNanos`) when a line matches. The regular expression can be specified via the `-match-regex` (alias `-regex`) parameter. 244 | 245 | ### Stopwatch regex template 246 | 247 | When using `-match-regex`, you can also specify a template `-match-template` (alias `-match`) to extract text from the current token. The output of this template is matched against the stopwatch regex. 248 | 249 | This allows you to use only specific fields of JSON objects as stopwatch reset triggers. For example: 250 | 251 | ```bash 252 | $ (echo {}; sleep 1; echo {}; sleep 1; echo '{"reset": "yes"}'; echo {}) | 253 | tj -read-json -match .reset -regex yes -template "{{.I}} {{.DeltaNanos}}" 254 | ``` 255 | 256 | ``` 257 | 0 14374 258 | 1 1005916918 259 | 2 2017292187 260 | 3 79099 261 | ``` 262 | 263 | The output of the match template is stored in the field `.MatchText` of the `token` struct: 264 | 265 | ```bash 266 | $ echo '{"message":"hello"}' | tj -read-json -match-template .message -template "{{.TimeString}} {{.MatchText}}" 267 | ``` 268 | 269 | ``` 270 | 2018-01-25T22:20:59+01:00 hello 271 | ``` 272 | 273 | ### Stopwatch condition 274 | 275 | Additionally to `-match-regex`, you can specify a `-match-condition` go template. If this template produces the literal string `true`, the stopwatch is reset - "matches" of the `-match-condition` are treated like matches of the `-match-regex`. 276 | 277 | ## Example 278 | 279 | Finding the slowest step in a `docker build` (using `jq`): 280 | 281 | ```bash 282 | $ cat Dockerfile 283 | FROM alpine 284 | RUN echo About to be slow... 285 | RUN sleep 10 286 | RUN echo Done being slow 287 | ``` 288 | 289 | ```bash 290 | $ docker build . | 291 | tj -regex ^Step | 292 | jq -s 'max_by(.deltaNanos) | {step:.start.text, duration:.delta}' 293 | ``` 294 | 295 | ```json 296 | {"step":"Step 3/4 : RUN sleep 10","duration":"10.602026127s"} 297 | ``` 298 | 299 | Alternatively, using color output and buffering: 300 | 301 | ```bash 302 | $ docker build . | 303 | tj -regex ^Step -match-buffer -template Color -scale-cube 304 | ``` 305 | 306 | ![Docker build with color output](docs/images/docker.png) 307 | 308 | ## Comments 309 | 310 | Feel free to [leave a comment](https://github.com/sgreben/tj/issues/1) or create an issue. -------------------------------------------------------------------------------- /cmd/tj/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strings" 12 | "text/template" 13 | "time" 14 | 15 | "github.com/sgreben/tj/pkg/color" 16 | ) 17 | 18 | type token struct { 19 | I uint64 `json:"-"` // token index 20 | TimeSecs int64 `json:"timeSecs"` 21 | TimeNanos int64 `json:"timeNanos"` 22 | TimeString string `json:"time,omitempty"` 23 | Time time.Time `json:"-"` 24 | DeltaSecs float64 `json:"deltaSecs"` 25 | DeltaNanos int64 `json:"deltaNanos"` 26 | DeltaString string `json:"delta,omitempty"` 27 | Delta time.Duration `json:"-"` 28 | TotalSecs float64 `json:"totalSecs"` 29 | TotalNanos int64 `json:"totalNanos"` 30 | TotalString string `json:"total,omitempty"` 31 | Total time.Duration `json:"-"` 32 | MatchText string `json:"-"` 33 | Start interface{} `json:"start,omitempty"` 34 | } 35 | 36 | func (t *token) copyDeltasFrom(token *token) { 37 | t.DeltaSecs = token.DeltaSecs 38 | t.DeltaNanos = token.DeltaNanos 39 | t.DeltaString = token.DeltaString 40 | t.Delta = token.Delta 41 | } 42 | 43 | type configuration struct { 44 | timeFormat string // -time-format="..." 45 | timeZone string // -time-zone="..." 46 | template string // -template="..." 47 | matchRegex string // -match-regex="..." 48 | matchTemplate string // -match-template="..." 49 | matchCondition string // -match-condition="..." 50 | buffer bool // -match-buffer 51 | readJSON bool // -read-json 52 | scaleText string // -scale="..." 53 | scaleFast time.Duration // -scale-fast="..." 54 | scaleSlow time.Duration // -scale-slow="..." 55 | scaleCube bool // -scale-cube 56 | scaleSqr bool // -scale-sqr 57 | scaleLinear bool // -scale-linear 58 | scaleSqrt bool // -scale-sqrt 59 | scaleCubert bool // -scale-cubert 60 | printVersionAndExit bool // -version 61 | } 62 | 63 | type printerFunc func(interface{}) error 64 | 65 | var ( 66 | version string 67 | config configuration 68 | printer printerFunc 69 | matchRegex *regexp.Regexp 70 | matchCondition *templateWithBuffer 71 | matchTemplate *templateWithBuffer 72 | scale color.Scale 73 | tokens tokenStream 74 | location *time.Location 75 | ) 76 | 77 | func print(data interface{}) { 78 | if err := printer(data); err != nil { 79 | fmt.Fprintln(os.Stderr, "output error:", err) 80 | } 81 | } 82 | 83 | const ISO8601 = "2006-01-02T15:04:05Z07:00" 84 | 85 | var timeFormats = map[string]string{ 86 | "ANSIC": time.ANSIC, 87 | "UnixDate": time.UnixDate, 88 | "RubyDate": time.RubyDate, 89 | "RFC822": time.RFC822, 90 | "RFC822Z": time.RFC822Z, 91 | "RFC850": time.RFC850, 92 | "RFC1123": time.RFC1123, 93 | "RFC1123Z": time.RFC1123Z, 94 | "RFC3339": time.RFC3339, 95 | "ISO8601": ISO8601, 96 | "RFC3339Nano": time.RFC3339Nano, 97 | "Kitchen": time.Kitchen, 98 | "Stamp": time.Stamp, 99 | "StampMilli": time.StampMilli, 100 | "StampMicro": time.StampMicro, 101 | "StampNano": time.StampNano, 102 | } 103 | 104 | var templates = map[string]string{ 105 | "Text": "{{.Text}}", 106 | "Time": "{{.TimeString}} {{.Text}}", 107 | "TimeDeltaNanos": "{{.TimeString}} +{{.DeltaNanos}} {{.Text}}", 108 | "TimeDelta": "{{.TimeString}} +{{.Delta}} {{.Text}}", 109 | "DeltaNanos": "{{.DeltaNanos}} {{.Text}}", 110 | "Delta": "{{.Delta}} {{.Text}}", 111 | "ColorText": "{{color .}}{{.Text}}{{reset}}", 112 | "Color": "{{color .}}█{{reset}} {{.Text}}", 113 | "DeltaColor": "{{.Delta}} {{color .}}█{{reset}} {{.Text}}", 114 | "TimeColor": "{{.TimeString}} {{color .}}█{{reset}} {{.Text}}", 115 | } 116 | 117 | var colorScales = map[string]string{ 118 | "GreenToRed": "#0F0 -> #F00", 119 | "GreenToGreenToRed": "#0F0 -> #0F0 -> #F00", 120 | "BlueToRed": "#00F -> #F00", 121 | "CyanToRed": "#0FF -> #F00", 122 | "WhiteToRed": "#FFF -> #F00", 123 | "WhiteToPurple": "#FFF -> #F700FF", 124 | "BlackToRed": "#000 -> #F00", 125 | "BlackToPurple": "#000 -> #F700FF", 126 | "WhiteToBlueToRed": "#FFF -> #00F -> #F00", 127 | } 128 | 129 | var templateFuncs = template.FuncMap{ 130 | "color": foregroundColor, 131 | "reset": func() string { return color.Reset }, 132 | } 133 | 134 | func foregroundColor(o tokenOwner) string { 135 | token := o.Token() 136 | c := float64(token.DeltaNanos-int64(config.scaleFast)) / float64(config.scaleSlow-config.scaleFast) 137 | return color.Foreground(scale(c)) 138 | } 139 | 140 | func jsonPrinter() printerFunc { 141 | enc := json.NewEncoder(os.Stdout) 142 | return enc.Encode 143 | } 144 | 145 | func templatePrinter(t string) printerFunc { 146 | template := template.Must(template.New("-template").Funcs(templateFuncs).Option("missingkey=zero").Parse(t)) 147 | newline := []byte("\n") 148 | return func(data interface{}) error { 149 | err := template.Execute(os.Stdout, data) 150 | os.Stdout.Write(newline) 151 | return err 152 | } 153 | } 154 | 155 | func timeFormatsHelp() string { 156 | help := []string{} 157 | for name, format := range timeFormats { 158 | help = append(help, fmt.Sprint("\t", name, " - ", format)) 159 | } 160 | sort.Strings(help) 161 | return "either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants)\n" + strings.Join(help, "\n") 162 | } 163 | 164 | func templatesHelp() string { 165 | help := []string{} 166 | for name, template := range templates { 167 | help = append(help, fmt.Sprint("\t", name, " - ", template)) 168 | } 169 | sort.Strings(help) 170 | return "either a go template (https://golang.org/pkg/text/template) or one of the predefined template names\n" + strings.Join(help, "\n") 171 | } 172 | 173 | func colorScalesHelp() string { 174 | help := []string{} 175 | for name, scale := range colorScales { 176 | help = append(help, fmt.Sprint("\t", name, " - ", scale)) 177 | } 178 | sort.Strings(help) 179 | return "either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow)\n" + strings.Join(help, "\n") 180 | } 181 | 182 | func addTemplateDelimitersIfLiteral(t string) string { 183 | if !strings.Contains(t, "{{") { 184 | return "{{" + t + "}}" 185 | } 186 | return t 187 | } 188 | 189 | func init() { 190 | flag.StringVar(&config.template, "template", "", templatesHelp()) 191 | flag.StringVar(&config.timeFormat, "time-format", "RFC3339", timeFormatsHelp()) 192 | flag.StringVar(&config.timeZone, "time-zone", "UTC", `time zone to use (or "Local")`) 193 | flag.StringVar(&config.matchRegex, "regex", "", "alias for -match-regex") 194 | flag.StringVar(&config.matchRegex, "match-regex", "", "a regex pattern. if given, only tokens matching it (re)start the stopwatch") 195 | flag.StringVar(&config.matchCondition, "condition", "", "alias for -match-condition") 196 | flag.StringVar(&config.matchCondition, "match-condition", "", "go template. if given, only tokens that result in 'true' (re)start the stopwatch") 197 | flag.StringVar(&config.matchTemplate, "match", "", "alias for -match-template") 198 | flag.StringVar(&config.matchTemplate, "match-template", "", "go template, used to extract text used for -match-regex") 199 | flag.BoolVar(&config.buffer, "match-buffer", false, "buffer lines between matches of -match-regex / -match-condition, copy delta values from final line to buffered lines") 200 | flag.BoolVar(&config.readJSON, "read-json", false, "parse a sequence of JSON objects from stdin") 201 | flag.StringVar(&config.scaleText, "scale", "BlueToRed", colorScalesHelp()) 202 | flag.DurationVar(&config.scaleFast, "scale-fast", 100*time.Millisecond, "the lower bound for the color scale") 203 | flag.DurationVar(&config.scaleSlow, "scale-slow", 2*time.Second, "the upper bound for the color scale") 204 | flag.BoolVar(&config.scaleCube, "scale-cube", false, "use cubic scale") 205 | flag.BoolVar(&config.scaleSqr, "scale-sqr", false, "use quadratic scale") 206 | flag.BoolVar(&config.scaleLinear, "scale-linear", true, "use linear scale") 207 | flag.BoolVar(&config.scaleSqrt, "scale-sqrt", false, "use quadratic root scale") 208 | flag.BoolVar(&config.scaleCubert, "scale-cubert", false, "use cubic root scale") 209 | flag.BoolVar(&config.printVersionAndExit, "version", false, "print version and exit") 210 | flag.Parse() 211 | if config.printVersionAndExit { 212 | fmt.Println(version) 213 | os.Exit(0) 214 | } 215 | var err error 216 | location, err = time.LoadLocation(config.timeZone) 217 | if err != nil { 218 | fmt.Fprintln(os.Stderr, "time zone parse error:", err) 219 | os.Exit(1) 220 | } 221 | if knownFormat, ok := timeFormats[config.timeFormat]; ok { 222 | config.timeFormat = knownFormat 223 | } 224 | if knownTemplate, ok := templates[config.template]; ok { 225 | config.template = knownTemplate 226 | } 227 | if knownScale, ok := colorScales[config.scaleText]; ok { 228 | config.scaleText = knownScale 229 | } 230 | if config.scaleText != "" { 231 | scale = color.ParseScale(config.scaleText) 232 | } 233 | if config.scaleLinear { 234 | // do nothing 235 | } 236 | if config.scaleSqrt { 237 | scale = color.Sqrt(scale) 238 | } 239 | if config.scaleCubert { 240 | scale = color.Cubert(scale) 241 | } 242 | if config.scaleSqr { 243 | scale = color.Sqr(scale) 244 | } 245 | if config.scaleCube { 246 | scale = color.Cube(scale) 247 | } 248 | if config.template != "" { 249 | printer = templatePrinter(config.template) 250 | } else { 251 | printer = jsonPrinter() 252 | } 253 | if config.matchRegex != "" { 254 | matchRegex = regexp.MustCompile(config.matchRegex) 255 | } 256 | if config.readJSON { 257 | tokens = newJSONStream() 258 | } else { 259 | tokens = newLineStream() 260 | } 261 | if config.matchTemplate != "" { 262 | config.matchTemplate = addTemplateDelimitersIfLiteral(config.matchTemplate) 263 | matchTemplate = &templateWithBuffer{ 264 | template: template.Must(template.New("-match-template").Option("missingkey=zero").Parse(config.matchTemplate)), 265 | buffer: bytes.NewBuffer(nil), 266 | } 267 | } 268 | if config.matchCondition != "" { 269 | config.matchCondition = addTemplateDelimitersIfLiteral(config.matchCondition) 270 | matchCondition = &templateWithBuffer{ 271 | template: template.Must(template.New("-match-condition").Option("missingkey=zero").Funcs(templateFuncs).Parse(config.matchCondition)), 272 | buffer: bytes.NewBuffer(nil), 273 | } 274 | } 275 | } 276 | 277 | type templateWithBuffer struct { 278 | template *template.Template 279 | buffer *bytes.Buffer 280 | } 281 | 282 | func (t *templateWithBuffer) executeSilent(data interface{}) (string, error) { 283 | t.buffer.Reset() 284 | err := t.template.Execute(t.buffer, data) 285 | return t.buffer.String(), err 286 | } 287 | 288 | func (t *templateWithBuffer) execute(data interface{}) string { 289 | s, err := t.executeSilent(data) 290 | if err != nil { 291 | fmt.Fprintln(os.Stderr, "template error:", err) 292 | } 293 | return s 294 | } 295 | 296 | type tokenOwner interface { 297 | Token() *token 298 | } 299 | 300 | type tokenStream interface { 301 | tokenOwner 302 | AppendCurrentToBuffer() 303 | FlushBuffer() 304 | CurrentMatchText() string 305 | CopyCurrent() tokenStream 306 | Err() error 307 | Scan() bool 308 | } 309 | 310 | func main() { 311 | token := tokens.Token() 312 | first := time.Now().In(location) 313 | last := first 314 | i := uint64(0) 315 | 316 | for tokens.Scan() { 317 | now := time.Now().In(location) 318 | delta := now.Sub(last) 319 | total := now.Sub(first) 320 | 321 | token.DeltaSecs = delta.Seconds() 322 | token.DeltaNanos = delta.Nanoseconds() 323 | token.DeltaString = delta.String() 324 | token.Delta = delta 325 | token.TotalSecs = total.Seconds() 326 | token.TotalNanos = total.Nanoseconds() 327 | token.TotalString = total.String() 328 | token.Total = total 329 | token.TimeSecs = now.Unix() 330 | token.TimeNanos = now.UnixNano() 331 | token.TimeString = now.Format(config.timeFormat) 332 | token.Time = now 333 | 334 | token.I = i 335 | token.MatchText = tokens.CurrentMatchText() 336 | 337 | matchRegexDefined := matchRegex != nil 338 | matchConditionDefined := matchCondition != nil 339 | matchDefined := matchRegexDefined || matchConditionDefined 340 | printToken := !matchDefined || !config.buffer 341 | 342 | matches := matchDefined 343 | if matchRegexDefined { 344 | matches = matches && matchRegex.MatchString(token.MatchText) 345 | } 346 | if matchConditionDefined { 347 | result, _ := matchCondition.executeSilent(tokens) 348 | matches = matches && strings.TrimSpace(result) == "true" 349 | } 350 | 351 | resetStopwatch := !matchDefined || matches 352 | 353 | if printToken { 354 | print(tokens) 355 | } 356 | if matches { 357 | currentCopy := tokens.CopyCurrent() 358 | currentCopy.Token().Start = nil // Prevent nested .start.start.start blow-up 359 | token.Start = currentCopy 360 | if config.buffer { 361 | tokens.FlushBuffer() 362 | } 363 | } 364 | if !printToken { 365 | tokens.AppendCurrentToBuffer() 366 | } 367 | if resetStopwatch { 368 | last = now 369 | } 370 | i++ 371 | } 372 | 373 | if config.buffer { 374 | tokens.FlushBuffer() 375 | } 376 | 377 | if err := tokens.Err(); err != nil { 378 | fmt.Fprintln(os.Stderr, "input error:", err) 379 | os.Exit(1) 380 | } 381 | } 382 | --------------------------------------------------------------------------------