272 lines
6.8 KiB
Go
272 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
)
|
|
|
|
var (
|
|
file = flag.String("f", "/var/log/haproxy/haproxy.log", "Path to the HAProxy log file (use - for stdin)")
|
|
top = flag.String("t", "conn", "Sort by connections or bytes sent (conn or bytes)")
|
|
num = flag.Int("n", 20, "Number of entries to display")
|
|
inc = flag.String("i", "all", "Information to display (all, ips, vhosts, requests)")
|
|
pattern = flag.String("p", "", "Filter by request pattern")
|
|
exclude = flag.String("e", "", "Exclude request pattern")
|
|
debug = flag.Bool("d", false, "Enable debug mode")
|
|
)
|
|
|
|
type IPData struct {
|
|
IP string
|
|
Count int
|
|
Bytes int64
|
|
Time int64
|
|
Request string
|
|
UserAgent string
|
|
}
|
|
|
|
type VhostData struct {
|
|
Vhost string
|
|
Count int
|
|
Bytes int64
|
|
Time int64
|
|
Request string
|
|
}
|
|
|
|
type ReqData struct {
|
|
Request string
|
|
Count int
|
|
Vhost string
|
|
Bytes int64
|
|
Time int64
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr, "Usage: %s [-f logfile] [-t conn|bytes] [-n num] [-p pattern] [-i all|ip|vhost|req] [-d]\n", os.Args[0])
|
|
flag.PrintDefaults()
|
|
fmt.Fprintf(os.Stderr, "\nRecommended haproxy log-format:\n")
|
|
fmt.Fprintf(os.Stderr, " log-format \"%%ci %%b/%%s %%ST %%B %%Tt %%sq/%%bq %%{+Q}r %%hr %%hs\"\n")
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
if *file == "-" {
|
|
*file = "/dev/stdin"
|
|
}
|
|
f, err := os.Open(*file)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
var errorLines []string
|
|
ipData := make(map[string]*IPData)
|
|
vhostData := make(map[string]*VhostData)
|
|
reqData := make(map[string]*ReqData)
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if *pattern != "" && !strings.Contains(line, *pattern) {
|
|
continue
|
|
}
|
|
if *exclude != "" && strings.Contains(line, *exclude) {
|
|
continue
|
|
}
|
|
fields := strings.Split(line, " ")
|
|
if len(fields) >= 7 {
|
|
ip := fields[3]
|
|
vhost := ""
|
|
userAgent := ""
|
|
if headers := strings.Split(line, "{"); len(headers) > 1 {
|
|
parts := strings.Split(headers[1], "|")
|
|
if len(parts) > 0 {
|
|
vhost = parts[0]
|
|
}
|
|
if len(parts) > 2 {
|
|
userAgent = strings.Split(parts[2], " ")[0]
|
|
userAgent = strings.Trim(userAgent, "}")
|
|
}
|
|
}
|
|
bytes, err := strconv.ParseInt(fields[6], 10, 64)
|
|
if err != nil {
|
|
errorLines = append(errorLines, line)
|
|
continue
|
|
}
|
|
time, err := strconv.ParseInt(fields[7], 10, 64)
|
|
if err != nil {
|
|
errorLines = append(errorLines, line)
|
|
continue
|
|
}
|
|
request := ""
|
|
if parts := strings.Split(line, "\""); len(parts) > 1 {
|
|
request = parts[1]
|
|
re := regexp.MustCompile(`http?s?:\/\/[^\/]+`)
|
|
request = re.ReplaceAllString(request, "")
|
|
request = strings.Split(request, " ")[1]
|
|
}
|
|
if data, ok := ipData[ip]; ok {
|
|
data.Count++
|
|
data.Bytes += bytes
|
|
data.Time += time
|
|
data.Request = request
|
|
data.UserAgent = userAgent
|
|
} else {
|
|
ipData[ip] = &IPData{IP: ip, Count: 1, Bytes: bytes, Time: time, Request: request, UserAgent: userAgent}
|
|
}
|
|
if data, ok := vhostData[vhost]; ok {
|
|
data.Count++
|
|
data.Bytes += bytes
|
|
data.Time += time
|
|
data.Request = request
|
|
} else if vhost != "" {
|
|
vhostData[vhost] = &VhostData{Vhost: vhost, Count: 1, Bytes: bytes, Time: time, Request: request}
|
|
}
|
|
if data, ok := reqData[request]; ok {
|
|
data.Count++
|
|
data.Bytes += bytes
|
|
data.Time += time
|
|
data.Vhost = vhost
|
|
} else if request != "" {
|
|
reqData[request] = &ReqData{Request: request, Count: 1, Bytes: bytes, Time: time, Vhost: vhost}
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
ipList := make([]*IPData, 0, len(ipData))
|
|
for _, data := range ipData {
|
|
ipList = append(ipList, data)
|
|
}
|
|
vhostList := make([]*VhostData, 0, len(vhostData))
|
|
for _, data := range vhostData {
|
|
vhostList = append(vhostList, data)
|
|
}
|
|
reqList := make([]*ReqData, 0, len(reqData))
|
|
for _, data := range reqData {
|
|
reqList = append(reqList, data)
|
|
}
|
|
|
|
sort.Slice(ipList, func(i, j int) bool {
|
|
switch *top {
|
|
case "bytes":
|
|
return ipList[i].Bytes > ipList[j].Bytes
|
|
default:
|
|
return ipList[i].Count > ipList[j].Count
|
|
}
|
|
})
|
|
sort.Slice(vhostList, func(i, j int) bool {
|
|
switch *top {
|
|
case "bytes":
|
|
return vhostList[i].Bytes > vhostList[j].Bytes
|
|
default:
|
|
return vhostList[i].Count > vhostList[j].Count
|
|
}
|
|
})
|
|
sort.Slice(reqList, func(i, j int) bool {
|
|
switch *top {
|
|
case "bytes":
|
|
return reqList[i].Bytes > reqList[j].Bytes
|
|
default:
|
|
return reqList[i].Count > reqList[j].Count
|
|
}
|
|
})
|
|
|
|
ipDisplay := *num
|
|
vhostDisplay := *num
|
|
reqDisplay := *num
|
|
|
|
if ipDisplay > len(ipList) {
|
|
ipDisplay = len(ipList)
|
|
}
|
|
if vhostDisplay > len(vhostList) {
|
|
vhostDisplay = len(vhostList)
|
|
}
|
|
if reqDisplay > len(reqList) {
|
|
reqDisplay = len(reqList)
|
|
}
|
|
|
|
w := new(tabwriter.Writer)
|
|
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
|
|
|
|
if *inc == "all" || strings.Contains(*inc, "ip") {
|
|
fmt.Fprintln(w, "Address\tCount\tBytes\tTime\tUser Agent\tLast Request")
|
|
fmt.Fprintln(w, "---------\t------\t---------\t-----\t-------------\t-------------")
|
|
for i := 0; i < ipDisplay; i++ {
|
|
data := ipList[i]
|
|
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\t%s\n", data.IP, data.Count, formatBytes(data.Bytes), formatTime(data.Time), data.UserAgent, data.Request)
|
|
}
|
|
fmt.Fprintln(w, "")
|
|
}
|
|
|
|
if *inc == "all" || strings.Contains(*inc, "vhost") {
|
|
fmt.Fprintln(w, "Vhost\tCount\tBytes\tTime\tLast Request")
|
|
fmt.Fprintln(w, "---------\t------\t---------\t-----\t-------------")
|
|
for i := 0; i < vhostDisplay; i++ {
|
|
data := vhostList[i]
|
|
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", data.Vhost, data.Count, formatBytes(data.Bytes), formatTime(data.Time), data.Request)
|
|
}
|
|
fmt.Fprintln(w, "")
|
|
}
|
|
|
|
if *inc == "all" || strings.Contains(*inc, "req") {
|
|
fmt.Fprintln(w, "Request\tCount\tBytes\tTime")
|
|
fmt.Fprintln(w, "---------\t------\t---------\t-----")
|
|
for i := 0; i < reqDisplay; i++ {
|
|
data := reqList[i]
|
|
fmt.Fprintf(w, "%s\t%d\t%s\t%s\n", data.Request, data.Count, formatBytes(data.Bytes), formatTime(data.Time))
|
|
}
|
|
fmt.Fprintln(w, "")
|
|
}
|
|
|
|
w.Flush()
|
|
|
|
if *debug {
|
|
if len(errorLines) > 0 {
|
|
fmt.Fprintln(os.Stderr, "Errors:")
|
|
for _, line := range errorLines {
|
|
fmt.Fprintln(os.Stderr, line)
|
|
}
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "No errors")
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
func formatTime(time int64) string {
|
|
switch {
|
|
case time < 1000:
|
|
return fmt.Sprintf("%dms", time)
|
|
case time < 1000*60:
|
|
return fmt.Sprintf("%ds", time/1000)
|
|
case time < 1000*60*60:
|
|
return fmt.Sprintf("%dm", time/(1000*60))
|
|
default:
|
|
return fmt.Sprintf("%dh", time/(1000*60*60))
|
|
}
|
|
}
|