Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A simple TUI-like interface #9

Merged
merged 15 commits into from
Sep 2, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Flags:
-p, --parser string Log parser (nginx-combined|nginx-json|caddy-json|goaccess) (default "nginx-json")
-r, --refresh int Refresh interval in seconds (default 5)
-s, --server string Server IP to filter (nginx-json only)
-S, --sort-by string Sort result by (size|requests) (default "size")
-t, --threshold size Threshold size for request (only requests at least this large will be counted) (default 10 MB)
-n, --top int Number of top items to show (default 10)
-w, --whole Analyze whole log file and then tail it
Expand Down
16 changes: 3 additions & 13 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ package cmd

import (
"fmt"
"net/netip"
"os"
"os/signal"
"runtime"
"syscall"
"time"

"github.com/spf13/cobra"
"github.com/taoky/ayano/pkg/analyze"
"github.com/taoky/ayano/pkg/systemd"
"github.com/taoky/ayano/pkg/tui"
)

const defaultFilename = "/var/log/nginx/mirrors/access_json.log"
Expand All @@ -23,15 +22,6 @@ func filenameFromArgs(args []string) string {
return args[0]
}

func printTopValuesRoutine(a *analyze.Analyzer) {
displayRecord := make(map[netip.Prefix]time.Time)
ticker := time.NewTicker(time.Duration(a.Config.RefreshSec) * time.Second)
for range ticker.C {
a.PrintTopValues(displayRecord, "size")
fmt.Println()
}
}

func runWithConfig(cmd *cobra.Command, args []string, config analyze.AnalyzerConfig) error {
filename := filenameFromArgs(args)
fmt.Fprintln(cmd.ErrOrStderr(), "Using log file:", filename)
Expand Down Expand Up @@ -59,7 +49,7 @@ func runWithConfig(cmd *cobra.Command, args []string, config analyze.AnalyzerCon
}

if !config.Analyze && !config.Daemon {
go printTopValuesRoutine(analyzer)
go tui.New(analyzer).Run()
}
if config.Daemon {
if err := systemd.NotifyReady(); err != nil {
Expand All @@ -70,7 +60,7 @@ func runWithConfig(cmd *cobra.Command, args []string, config analyze.AnalyzerCon
analyzer.RunLoop(iterator)

if config.Analyze {
analyzer.PrintTopValues(nil, config.SortBy)
analyzer.PrintTopValues(nil, config.SortBy, "")
}
return nil
}
Expand Down
92 changes: 73 additions & 19 deletions pkg/analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ type IPStats struct {
LastURLAccess time.Time
}

type StatKey struct {
Server string
Prefix netip.Prefix
}

type Analyzer struct {
Config AnalyzerConfig

ipInfo map[netip.Prefix]IPStats
mu sync.Mutex
// [server, ip prefix] -> IPStats
stats map[StatKey]IPStats
mu sync.Mutex

logParser parser.Parser
logger *log.Logger
Expand All @@ -56,7 +62,7 @@ type AnalyzerConfig struct {
Parser string
RefreshSec int
Server string
SortBy string
SortBy SortByFlag
Threshold SizeFlag
TopN int
Whole bool
Expand All @@ -72,7 +78,7 @@ func (c *AnalyzerConfig) InstallFlags(flags *pflag.FlagSet) {
flags.StringVarP(&c.Parser, "parser", "p", c.Parser, "Log parser (nginx-combined|nginx-json|caddy-json|goaccess)")
flags.IntVarP(&c.RefreshSec, "refresh", "r", c.RefreshSec, "Refresh interval in seconds")
flags.StringVarP(&c.Server, "server", "s", c.Server, "Server IP to filter (nginx-json only)")
flags.StringVarP(&c.SortBy, "sort-by", "S", c.SortBy, "Sort result by (size|requests)")
flags.VarP(&c.SortBy, "sort-by", "S", "Sort result by (size|requests)")
flags.VarP(&c.Threshold, "threshold", "t", "Threshold size for request (only requests at least this large will be counted)")
flags.IntVarP(&c.TopN, "top", "n", c.TopN, "Number of top items to show")
flags.BoolVarP(&c.Whole, "whole", "w", c.Whole, "Analyze whole log file and then tail it")
Expand All @@ -85,7 +91,7 @@ func DefaultConfig() AnalyzerConfig {
return AnalyzerConfig{
Parser: "nginx-json",
RefreshSec: 5,
SortBy: "size",
SortBy: SortBySize,
Threshold: SizeFlag(10e6),
TopN: 10,
}
Expand Down Expand Up @@ -113,7 +119,7 @@ func NewAnalyzer(c AnalyzerConfig) (*Analyzer, error) {

return &Analyzer{
Config: c,
ipInfo: make(map[netip.Prefix]IPStats),
stats: make(map[StatKey]IPStats),
logParser: logParser,
logger: logger,
}, nil
Expand Down Expand Up @@ -158,7 +164,7 @@ func (a *Analyzer) handleLine(line []byte) error {
a.mu.Lock()
defer a.mu.Unlock()
}
ipStats := a.ipInfo[clientPrefix]
ipStats := a.stats[StatKey{logItem.Server, clientPrefix}]

ipStats.Size += size
ipStats.Requests += 1
Expand All @@ -185,10 +191,11 @@ func (a *Analyzer) handleLine(line []byte) error {
}
ipStats.LastSize = ipStats.Size
}
a.ipInfo[clientPrefix] = ipStats
a.stats[StatKey{logItem.Server, clientPrefix}] = ipStats
return nil
}
func (a *Analyzer) PrintTopValues(displayRecord map[netip.Prefix]time.Time, sortBy string) {

func (a *Analyzer) PrintTopValues(displayRecord map[netip.Prefix]time.Time, sortBy SortByFlag, serverFilter string) {
activeConn := make(map[netip.Prefix]int)
if !a.Config.NoNetstat {
// Get active connections
Expand Down Expand Up @@ -228,11 +235,14 @@ func (a *Analyzer) PrintTopValues(displayRecord map[netip.Prefix]time.Time, sort
}

// sort stats key by value
keys := make([]netip.Prefix, 0, len(a.ipInfo))
for k := range a.ipInfo {
keys = append(keys, k)
keys := make([]StatKey, 0)
for s := range a.stats {
if serverFilter != "" && s.Server != serverFilter {
continue
}
keys = append(keys, s)
}
sortFunc := GetSortFunc(sortBy, a.ipInfo)
sortFunc := GetSortFunc(sortBy, a.stats)
if sortFunc != nil {
slices.SortFunc(keys, sortFunc)
}
Expand All @@ -248,7 +258,7 @@ func (a *Analyzer) PrintTopValues(displayRecord map[netip.Prefix]time.Time, sort

for i := range top {
key := keys[i]
ipStats := a.ipInfo[key]
ipStats := a.stats[key]
total := ipStats.Size
reqTotal := ipStats.Requests
last := ipStats.LastURL
Expand All @@ -270,15 +280,15 @@ func (a *Analyzer) PrintTopValues(displayRecord map[netip.Prefix]time.Time, sort
connection := ""
boldLine := false

if displayRecord != nil && displayRecord[key] != ipStats.LastURLAccess {
if displayRecord != nil && displayRecord[key.Prefix] != ipStats.LastURLAccess {
// display this line in bold
fmtStart = boldStart
fmtEnd = boldEnd
boldLine = true
}
if !a.Config.NoNetstat {
if _, ok := activeConn[key]; ok {
activeString := fmt.Sprintf(" (%2d)", activeConn[key])
if _, ok := activeConn[key.Prefix]; ok {
activeString := fmt.Sprintf(" (%2d)", activeConn[key.Prefix])
if !boldLine {
connection = fmt.Sprintf("%s%s%s", boldStart, activeString, boldEnd)
} else {
Expand All @@ -288,10 +298,54 @@ func (a *Analyzer) PrintTopValues(displayRecord map[netip.Prefix]time.Time, sort
connection = " "
}
}
a.logger.Printf("%s%16s%s: %7s %3d %7s %s (from %s, last accessed %s)%s\n", fmtStart, key, connection, humanize.IBytes(total), reqTotal,
a.logger.Printf("%s%16s%s: %7s %3d %7s %s (from %s, last accessed %s)%s\n", fmtStart, key.Prefix, connection, humanize.IBytes(total), reqTotal,
humanize.IBytes(average), last, lastUpdateTime, lastAccessTime, fmtEnd)
if displayRecord != nil {
displayRecord[key] = ipStats.LastURLAccess
displayRecord[key.Prefix] = ipStats.LastURLAccess
}
}
}

func (a *Analyzer) GetCurrentServers() []string {
if a.Config.UseLock() {
a.mu.Lock()
defer a.mu.Unlock()
}
servers := make(map[string]struct{})
for sp := range a.stats {
servers[sp.Server] = struct{}{}
}
keys := make([]string, 0, len(servers))
for key := range servers {
keys = append(keys, key)
}
return keys
}

func (a *Analyzer) PrintTotal() {
type kv struct {
server string
value uint64
}
if a.Config.UseLock() {
a.mu.Lock()
defer a.mu.Unlock()
}

totals := make(map[string]uint64)
for sp, value := range a.stats {
totals[sp.Server] += value.Size
}

var totalSlice []kv
for k, v := range totals {
totalSlice = append(totalSlice, kv{k, v})
}
slices.SortFunc(totalSlice, func(i, j kv) int {
return int(j.value - i.value)
})

for _, kv := range totalSlice {
a.logger.Printf("%s: %s\n", kv.server, humanize.IBytes(kv.value))
}
}
2 changes: 1 addition & 1 deletion pkg/analyze/analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func benchmarkAnalyzeLoop(b *testing.B, parserStr string) {
}

a.RunLoop(t)
a.PrintTopValues(nil, "size")
a.PrintTopValues(nil, SortBySize, "")
}

func BenchmarkAnalyzeLoopNgxJSON(b *testing.B) {
Expand Down
23 changes: 9 additions & 14 deletions pkg/analyze/sortfunc.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
package analyze

import (
"net/netip"
"slices"
)

type SortFunc func(l, r netip.Prefix) int
type SortFunc func(l, r StatKey) int

var sortFuncs = map[string]func(a map[netip.Prefix]IPStats) SortFunc{
"size": func(i map[netip.Prefix]IPStats) SortFunc {
return func(l, r netip.Prefix) int {
var sortFuncs = map[SortByFlag]func(a map[StatKey]IPStats) SortFunc{
SortBySize: func(i map[StatKey]IPStats) SortFunc {
return func(l, r StatKey) int {
return int(i[r].Size - i[l].Size)
}
},
"requests": func(i map[netip.Prefix]IPStats) SortFunc {
return func(l, r netip.Prefix) int {
SortByRequests: func(i map[StatKey]IPStats) SortFunc {
return func(l, r StatKey) int {
return int(i[r].Requests - i[l].Requests)
}
},
}

func init() {
sortFuncs["reqs"] = sortFuncs["requests"]
}

func GetSortFunc(name string, i map[netip.Prefix]IPStats) SortFunc {
func GetSortFunc(name SortByFlag, i map[StatKey]IPStats) SortFunc {
fn, ok := sortFuncs[name]
if !ok {
return nil
}
return fn(i)
}

func ListSortFuncs() []string {
ret := make([]string, 0, len(sortFuncs))
func ListSortFuncs() []SortByFlag {
ret := make([]SortByFlag, 0, len(sortFuncs))
for key := range sortFuncs {
ret = append(ret, key)
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/analyze/util.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package analyze

import (
"errors"
"net/netip"
"strconv"

Expand Down Expand Up @@ -33,6 +34,33 @@ func (s SizeFlag) Type() string {
return "size"
}

type SortByFlag string

const (
SortBySize SortByFlag = "size"
SortByRequests SortByFlag = "requests"
)

func (s SortByFlag) String() string {
return string(s)
}

func (s *SortByFlag) Set(value string) error {
switch value {
case "size":
*s = SortBySize
case "requests", "reqs":
*s = SortByRequests
default:
return errors.New(`must be one of "size" or "requests"`)
}
return nil
}

func (s SortByFlag) Type() string {
return "string"
}

func IPPrefix(ip netip.Addr) netip.Prefix {
var clientPrefix netip.Prefix
if ip.Is4() {
Expand Down
Loading
Loading