Files
topip/topip.go
tchivert c9e52c78a4
build / build (push) Successful in 33s
add exclusion
2024-12-29 22:51:37 +01:00

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))
}
}