Skip to content

Commit

Permalink
Merge pull request #73 from tkuchiki/add-count-subcommand
Browse files Browse the repository at this point in the history
Add count subcommand
  • Loading branch information
tkuchiki committed Jul 22, 2023
2 parents e52b64d + f92a5d2 commit 9cef20e
Show file tree
Hide file tree
Showing 13 changed files with 559 additions and 23 deletions.
15 changes: 15 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Usage:

Available Commands:
completion Generate the autocompletion script for the specified shell
count Count by log entries
diff Show the difference between the two profile results
help Help about any command
json Profile the logs for JSON
Expand Down Expand Up @@ -216,6 +217,20 @@ Flags:
--from string The comparison source file
-h, --help help for diff
--to string The comparison target file

$ alp count --help
Count by log entries

Usage:
alp count [flags]

Flags:
--file string The access log file
--format string Log format (json,ltsv,regexp) (default "json")
-h, --help help for count
--keys string Log key names (comma separated)
--pattern string Regular expressions pattern matching the log. (only use with --format=regexp) (default "^(\\S+)\\s\\S+\\s+(\\S+\\s+)+\\[(?P<time>[^]]+)\\]\\s\"(?P<method>\\S*)\\s?(?P<uri>(?:[^\"]*(?:\\\\\")?)*)\\s([^\"]*)\"\\s(?P<status>\\S+)\\s(?P<body_bytes>\\S+)\\s\"((?:[^\"]*(?:\\\\\")?)*)\"\\s\"(?:.+)\"\\s(?P<response_time>\\S+)(?:\\s(?P<request_time>\\S+))?$")
-r, --reverse Sort results in reverse order
```

- alp は ltsv, json, regexp, pcap, diff の5つのサブコマンドで構成されています
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Usage:

Available Commands:
completion Generate the autocompletion script for the specified shell
count Count by log entries
diff Show the difference between the two profile results
help Help about any command
json Profile the logs for JSON
Expand Down Expand Up @@ -217,6 +218,20 @@ Flags:
--from string The comparison source file
-h, --help help for diff
--to string The comparison target file

$ alp count --help
Count by log entries

Usage:
alp count [flags]

Flags:
--file string The access log file
--format string Log format (json,ltsv,regexp) (default "json")
-h, --help help for count
--keys string Log key names (comma separated)
--pattern string Regular expressions pattern matching the log. (only use with --format=regexp) (default "^(\\S+)\\s\\S+\\s+(\\S+\\s+)+\\[(?P<time>[^]]+)\\]\\s\"(?P<method>\\S*)\\s?(?P<uri>(?:[^\"]*(?:\\\\\")?)*)\\s([^\"]*)\"\\s(?P<status>\\S+)\\s(?P<body_bytes>\\S+)\\s\"((?:[^\"]*(?:\\\\\")?)*)\"\\s\"(?:.+)\"\\s(?P<response_time>\\S+)(?:\\s(?P<request_time>\\S+))?$")
-r, --reverse Sort results in reverse order
```

## ltsv
Expand Down
98 changes: 98 additions & 0 deletions cmd/alp/cmd/count.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
"github.com/tkuchiki/alp/counter"
"github.com/tkuchiki/alp/helpers"
"github.com/tkuchiki/alp/options"
"github.com/tkuchiki/alp/parsers"
)

func NewCountCmd() *cobra.Command {
var countCmd = &cobra.Command{
Use: "count",
Short: "Count by log entries",
Long: `Count by log entries`,
RunE: func(cmd *cobra.Command, args []string) error {
file, err := cmd.PersistentFlags().GetString("file")
if err != nil {
return err
}

reverse, err := cmd.PersistentFlags().GetBool("reverse")
if err != nil {
return err
}

pattern, err := cmd.PersistentFlags().GetString("pattern")
if err != nil {
return err
}

opts := options.NewOptions()

opts = options.SetOptions(opts,
options.File(file),
options.Reverse(reverse),
options.Pattern(pattern),
)

format, err := cmd.PersistentFlags().GetString("format")
if err != nil {
return err
}

keysStr, err := cmd.PersistentFlags().GetString("keys")
if err != nil {
return err
}

keys := helpers.SplitCSV(keysStr)

cnter := counter.NewCounter(os.Stdout, os.Stderr, opts.Reverse)

f, err := cnter.Open(opts.File)
if err != nil {
return err
}
defer f.Close()

var parser parsers.Parser
switch format {
case "json":
jsonKeys := parsers.NewJSONKeys(opts.JSON.UriKey, opts.JSON.MethodKey, opts.JSON.TimeKey,
opts.JSON.ResponseTimeKey, opts.JSON.RequestTimeKey, opts.JSON.BodyBytesKey, opts.JSON.StatusKey)
parser = parsers.NewJSONParser(f, jsonKeys, opts.QueryString, opts.QueryStringIgnoreValues)
case "ltsv":
label := parsers.NewLTSVLabel(opts.LTSV.UriLabel, opts.LTSV.MethodLabel, opts.LTSV.TimeLabel,
opts.LTSV.ApptimeLabel, opts.LTSV.ReqtimeLabel, opts.LTSV.SizeLabel, opts.LTSV.StatusLabel,
)
parser = parsers.NewLTSVParser(f, label, opts.QueryString, opts.QueryStringIgnoreValues)
case "regexp":
names := parsers.NewSubexpNames(opts.Regexp.UriSubexp, opts.Regexp.MethodSubexp, opts.Regexp.TimeSubexp,
opts.Regexp.ResponseTimeSubexp, opts.Regexp.RequestTimeSubexp, opts.Regexp.BodyBytesSubexp, opts.Regexp.StatusSubexp)
parser, err = parsers.NewRegexpParser(f, opts.Regexp.Pattern, names, opts.QueryString, opts.QueryStringIgnoreValues)
if err != nil {
return err
}
}

cnter.SetParser(parser)

err = cnter.CountAndPrint(keys)

return err
},
}

countCmd.PersistentFlags().StringP("format", "", "json", "Log format (json,ltsv,regexp)")
countCmd.PersistentFlags().StringP("pattern", "", options.DefaultPatternOption, "Regular expressions pattern matching the log (only use with --format=regexp)")
countCmd.PersistentFlags().StringP("file", "", "", "The access log file")
countCmd.PersistentFlags().BoolP("reverse", "r", false, "Sort results in reverse order")
countCmd.PersistentFlags().StringP("keys", "", "", "Log key names (comma separated)")
countCmd.MarkPersistentFlagRequired("keys")

return countCmd
}
2 changes: 1 addition & 1 deletion cmd/alp/cmd/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func defineOptions(cmd *cobra.Command) {
cmd.PersistentFlags().StringP("config", "", "", "The configuration file")
cmd.PersistentFlags().StringP("file", "", "", "The slowlog file")
cmd.PersistentFlags().StringP("file", "", "", "The access log file")
cmd.PersistentFlags().StringP("dump", "", "", "Dump profiled data as YAML")
cmd.PersistentFlags().StringP("load", "", "", "Load the profiled YAML data")
cmd.PersistentFlags().StringP("format", "", options.DefaultFormatOption, "The output format (table, markdown, tsv, csv and html)")
Expand Down
1 change: 1 addition & 0 deletions cmd/alp/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func NewRootCmd(version string) *cobra.Command {
rootCmd.AddCommand(NewRegexpCmd(rootCmd))
rootCmd.AddCommand(NewPcapCmd(rootCmd))
rootCmd.AddCommand(NewDiffCmd(rootCmd))
rootCmd.AddCommand(NewCountCmd())
rootCmd.SetVersionTemplate(fmt.Sprintln(version))

return rootCmd
Expand Down
178 changes: 178 additions & 0 deletions counter/counter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package counter

import (
"io"
"os"
"sort"
"strings"
"sync"

"github.com/tkuchiki/alp/errors"
"github.com/tkuchiki/alp/parsers"
)

type Counter struct {
outWriter io.Writer
errWriter io.Writer
inReader *os.File
parser parsers.Parser
groups *groups
printer *Printer
reverse bool
}

func NewCounter(outw, errw io.Writer, reverse bool) *Counter {
return &Counter{
outWriter: outw,
errWriter: errw,
inReader: os.Stdin,
printer: NewPrinter(outw),
groups: newGroups(),
reverse: reverse,
}
}

func (c *Counter) SetParser(parser parsers.Parser) {
c.parser = parser
}

func (c *Counter) SetInReader(f *os.File) {
c.inReader = f
}

func (c *Counter) Open(filename string) (*os.File, error) {
var f *os.File
var err error

if filename != "" {
f, err = os.Open(filename)
} else {
f = c.inReader
}

return f, err
}

func (c *Counter) Count(keys []string) error {
c.groups.keys = keys

Loop:
for {
s, err := c.parser.Parse()
if err != nil {
if err == io.EOF {
break
} else if err == errors.SkipReadLineErr {
continue Loop
}

return err
}

entries := make([]string, len(keys))
group := newGroup()
for _, key := range keys {
val, ok := s.Entries[key]
if !ok {
continue Loop
}
entries = append(entries, val)
group.values[key] = val
}

concatenatedKey := strings.Join(entries, "_")
idx := c.groups.hints.loadOrStore(concatenatedKey)

if idx >= len(c.groups.groups) {
c.groups.groups = append(c.groups.groups, group)
}

c.groups.groups[idx].count++
}

return nil
}

func (c *Counter) Sort() {
if c.reverse {
sort.Slice(c.groups.groups, func(i, j int) bool {
return c.groups.groups[i].count > c.groups.groups[j].count
})
} else {
sort.Slice(c.groups.groups, func(i, j int) bool {
return c.groups.groups[i].count < c.groups.groups[j].count
})
}
}

func (c *Counter) Print() {
c.Sort()
c.printer.Print(c.groups)
}

func (c *Counter) CountAndPrint(keys []string) error {
err := c.Count(keys)
if err != nil {
return err
}

c.Print()

return nil
}

type hints struct {
values map[string]int
len int
mu sync.RWMutex
}

func newHints() *hints {
return &hints{
values: make(map[string]int),
}
}

func (h *hints) loadOrStore(key string) int {
h.mu.Lock()
defer h.mu.Unlock()
_, ok := h.values[key]
if !ok {
h.values[key] = h.len
h.len++
}

return h.values[key]
}

type groups struct {
keys []string
groups []*group
hints *hints
}

func newGroups() *groups {
return &groups{
hints: newHints(),
}
}

type group struct {
values map[string]string
count int64
}

func newGroup() *group {
return &group{
values: make(map[string]string),
count: 0,
}
}

func (g *group) incr() {
g.count++
}

func (g *group) getCount() int64 {
return g.count
}
Loading

0 comments on commit 9cef20e

Please sign in to comment.