Skip to content

Commit 4393541

Browse files
committed
Adding parselogs
Adding the parselogs utility
1 parent deb558b commit 4393541

File tree

4 files changed

+181
-0
lines changed

4 files changed

+181
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ cmd/chunktool/chunktool
66
images/
77
dist/
88
.DS_STORE
9+
cmd/parselogs/parselogs

cmd/parselogs/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
all: build-bin
2+
3+
build-bin:
4+
GO111MODULE=on go build -o parselogs -v ./
5+
6+
build: build-bin

cmd/parselogs/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# parselogs
2+
3+
A cli tool to parse Cortex query-frontend logs and format them for easy analysis.
4+
5+
```
6+
Options:
7+
-dur duration
8+
only show queries which took longer than this duration, e.g. -dur 10s
9+
-query
10+
show the query
11+
-utc
12+
show timestamp in UTC time
13+
```
14+
15+
Feed logs into it using `logcli` from Loki, `kubectl` from Kubernetes, `cat` from a file, or any other way to get raw logs:
16+
17+
```
18+
$ cat query-frontend-logs.log | ./parselogs -dur 5s
19+
Timestamp TraceID Length Duration Status Path
20+
2020-05-26 13:51:15.0577354 -0400 EDT 76b9939fd5c78b8f 6h0m0s 10.249149614s (200) /api/prom/api/v1/query_range
21+
2020-05-26 13:52:15.771988849 -0400 EDT 2e7473ab10160630 10h33m0s 7.472855362s (200) /api/prom/api/v1/query_range
22+
2020-05-26 13:53:46.712563497 -0400 EDT 761f3221dcdd85de 10h33m0s 11.874296689s (200) /api/prom/api/v1/query_range
23+
```

cmd/parselogs/main.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"flag"
6+
"fmt"
7+
"net/url"
8+
"os"
9+
"regexp"
10+
"strconv"
11+
"strings"
12+
"text/tabwriter"
13+
"time"
14+
)
15+
16+
var (
17+
rex = regexp.MustCompile("(\\S+)=(\".*?\"|\\S+)")
18+
layout = "2006-01-02T15:04:05.999999999Z"
19+
)
20+
21+
func main() {
22+
23+
fi, _ := os.Stdin.Stat()
24+
isPipe := false
25+
if (fi.Mode() & os.ModeCharDevice) == 0 {
26+
isPipe = true
27+
}
28+
29+
showQuery := flag.Bool("query", false, "show the query")
30+
minDur := flag.Duration("dur", 0, "only show queries which took longer than this duration, e.g. 10s")
31+
utc := flag.Bool("utc", false, "show timestamp in UTC time")
32+
flag.Parse()
33+
34+
// use tabwriter to format the output spaced out
35+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
36+
37+
// headings
38+
if *showQuery {
39+
fmt.Fprintf(w, "Timestamp\tTraceID\tLength\tDuration\tStatus\tPath\tQuery\n")
40+
} else {
41+
fmt.Fprintf(w, "Timestamp\tTraceID\tLength\tDuration\tStatus\tPath\n")
42+
}
43+
44+
scanner := bufio.NewScanner(os.Stdin)
45+
for scanner.Scan() {
46+
show := false
47+
var qt time.Time
48+
var dur, len time.Duration
49+
var status, path, query, trace string
50+
var err error
51+
52+
line := scanner.Text()
53+
data := rex.FindAllStringSubmatch(line, -1)
54+
55+
for _, d := range data {
56+
switch d[1] {
57+
case "traceID":
58+
trace = d[2]
59+
case "ts":
60+
qt, err = time.Parse(layout, d[2])
61+
if err != nil {
62+
fmt.Println(err, line)
63+
continue
64+
}
65+
case "msg":
66+
msg := strings.ReplaceAll(d[2], "\"", "")
67+
if !(strings.HasPrefix(msg, "GET /loki/api/") ||
68+
strings.HasPrefix(msg, "GET /api/prom")) {
69+
continue
70+
}
71+
72+
show = true
73+
74+
parts := strings.Split(msg, " ")
75+
u, err := url.Parse(parts[1])
76+
if err != nil {
77+
fmt.Println(err, line)
78+
continue
79+
}
80+
vals, err := url.ParseQuery(u.RawQuery)
81+
if err != nil {
82+
fmt.Println(err, line)
83+
continue
84+
}
85+
start := vals.Get("start")
86+
end := vals.Get("end")
87+
88+
if start != "" && end != "" {
89+
var st, et int64
90+
//First try to parse as unix sec timestamps
91+
st, err = strconv.ParseInt(start, 10, 64)
92+
et, err = strconv.ParseInt(end, 10, 64)
93+
if err != nil {
94+
//Next try to parse as time/date
95+
var st, et time.Time
96+
st, err = time.Parse(layout, start)
97+
et, err = time.Parse(layout, end)
98+
if err != nil {
99+
fmt.Println(err, line)
100+
continue
101+
}
102+
len = et.Sub(st)
103+
} else {
104+
// Loki queries are nanosecond, simple check to see if it's a second or nanosecond timestamp
105+
if st > 9999999999 {
106+
len = time.Unix(0, et).Sub(time.Unix(0, st))
107+
} else {
108+
len = time.Unix(et, 0).Sub(time.Unix(st, 0))
109+
}
110+
111+
}
112+
113+
}
114+
115+
status = parts[2]
116+
path = u.Path
117+
query = vals.Get("query")
118+
119+
dur, err = time.ParseDuration(parts[3])
120+
if err != nil {
121+
fmt.Println(err, line)
122+
continue
123+
}
124+
}
125+
}
126+
127+
if show && dur > *minDur {
128+
var ts string
129+
if *utc {
130+
ts = fmt.Sprint(qt)
131+
} else {
132+
ts = fmt.Sprint(qt.Local())
133+
}
134+
if *showQuery {
135+
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\n", ts, trace, len, dur, status, path, query)
136+
} else {
137+
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", ts, trace, len, dur, status, path)
138+
}
139+
//If looking at stdin, flush after every line as someone would only paste one line at a time at the terminal
140+
if !isPipe {
141+
w.Flush()
142+
}
143+
144+
}
145+
show = false
146+
}
147+
148+
fmt.Printf("\n")
149+
w.Flush()
150+
151+
}

0 commit comments

Comments
 (0)