From a0913aeeb7987982dff71d42896d926a367ba9f5 Mon Sep 17 00:00:00 2001 From: tkuchiki Date: Sat, 22 Jul 2023 17:24:08 +0900 Subject: [PATCH 1/2] Add count subcommand --- cmd/alp/cmd/count.go | 98 ++++++++++++++++++++ cmd/alp/cmd/option.go | 2 +- cmd/alp/cmd/root.go | 1 + counter/counter.go | 178 ++++++++++++++++++++++++++++++++++++ counter/printer.go | 205 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 18 ++-- go.sum | 7 -- parsers/json.go | 14 ++- parsers/ltsv.go | 12 ++- parsers/parser.go | 3 + parsers/regexp.go | 14 ++- 11 files changed, 529 insertions(+), 23 deletions(-) create mode 100644 cmd/alp/cmd/count.go create mode 100644 counter/counter.go create mode 100644 counter/printer.go diff --git a/cmd/alp/cmd/count.go b/cmd/alp/cmd/count.go new file mode 100644 index 0000000..02616a1 --- /dev/null +++ b/cmd/alp/cmd/count.go @@ -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 +} diff --git a/cmd/alp/cmd/option.go b/cmd/alp/cmd/option.go index 88f2b4b..314144d 100644 --- a/cmd/alp/cmd/option.go +++ b/cmd/alp/cmd/option.go @@ -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)") diff --git a/cmd/alp/cmd/root.go b/cmd/alp/cmd/root.go index fe8a61c..4dda400 100644 --- a/cmd/alp/cmd/root.go +++ b/cmd/alp/cmd/root.go @@ -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 diff --git a/counter/counter.go b/counter/counter.go new file mode 100644 index 0000000..35649cc --- /dev/null +++ b/counter/counter.go @@ -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 +} diff --git a/counter/printer.go b/counter/printer.go new file mode 100644 index 0000000..884f699 --- /dev/null +++ b/counter/printer.go @@ -0,0 +1,205 @@ +package counter + +import ( + "fmt" + "io" + + "github.com/olekukonko/tablewriter" +) + +const ( + defaultSumHeader = "sum" +) + +type Printer struct { + format string + printOptions *PrintOptions + writer io.Writer +} + +type PrintOptions struct { + noHeaders bool + showFooters bool + paginationLimit int +} + +func NewPrintOptions(noHeaders, showFooters bool, paginationLimit int) *PrintOptions { + return &PrintOptions{ + noHeaders: noHeaders, + showFooters: showFooters, + paginationLimit: paginationLimit, + } +} + +func NewPrinter(w io.Writer) *Printer { + return &Printer{ + format: "table", + writer: w, + } +} + +func (p *Printer) Print(groups *groups) { + + switch p.format { + case "table": + p.printTable(groups) + /*case "md", "markdown": + p.printMarkdown(groups) + case "tsv": + p.printTSV(groups) + case "csv": + p.printCSV(groups) + case "html": + p.printHTML(groups)*/ + } +} + +func (p *Printer) generateLine(keys []string, group *group) []string { + var data []string + data = append(data, fmt.Sprint(group.getCount())) + for _, key := range keys { + data = append(data, group.values[key]) + } + + return data +} + +func (p *Printer) printTable(groups *groups) { + table := tablewriter.NewWriter(p.writer) + var headers []string + headers = append(headers, defaultSumHeader) + headers = append(headers, groups.keys...) + table.SetHeader(headers) + + for _, group := range groups.groups { + data := p.generateLine(groups.keys, group) + table.Append(data) + } + + /* + if p.printOptions.showFooters { + var footer []string + if hsTo == nil { + footer = p.GenerateFooter(hsFrom.CountAll()) + } else { + footer = p.GenerateFooterWithDiff(hsFrom.CountAll(), hsTo.CountAll()) + } + table.SetFooter(footer) + table.SetFooterAlignment(tablewriter.ALIGN_LEFT) + }*/ + + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.Render() +} + +/* +func (p *Printer) printMarkdown(groups) { + table := tablewriter.NewWriter(p.writer) + table.SetHeader(p.headers) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + if hsTo == nil { + for _, s := range hsFrom.stats { + data := p.GenerateLine(s, false) + table.Append(data) + } + } else { + for _, to := range hsTo.stats { + from := findHTTPStatFrom(hsFrom, to) + + var data []string + if from == nil { + data = p.GenerateLine(to, false) + } else { + data = p.GenerateLineWithDiff(from, to, false) + } + table.Append(data) + } + } + + if p.printOptions.showFooters { + var footer []string + if hsTo == nil { + footer = p.GenerateFooter(hsFrom.CountAll()) + } else { + footer = p.GenerateFooterWithDiff(hsFrom.CountAll(), hsTo.CountAll()) + } + table.Append(footer) + } + + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.Render() +} + +func (p *Printer) printTSV(groups) { + if !p.printOptions.noHeaders { + fmt.Println(strings.Join(p.headers, "\t")) + } + + var data []string + if hsTo == nil { + for _, s := range hsFrom.stats { + data = p.GenerateLine(s, false) + fmt.Println(strings.Join(data, "\t")) + } + } else { + for _, to := range hsTo.stats { + from := findHTTPStatFrom(hsFrom, to) + + if from == nil { + data = p.GenerateLine(to, false) + } else { + data = p.GenerateLineWithDiff(from, to, false) + } + fmt.Println(strings.Join(data, "\t")) + } + } +} + +func (p *Printer) printCSV(groups) { + if !p.printOptions.noHeaders { + fmt.Println(strings.Join(p.headers, ",")) + } + + var data []string + if hsTo == nil { + for _, s := range hsFrom.stats { + data = p.GenerateLine(s, true) + fmt.Println(strings.Join(data, ",")) + } + } else { + for _, to := range hsTo.stats { + from := findHTTPStatFrom(hsFrom, to) + + if from == nil { + data = p.GenerateLine(to, false) + } else { + data = p.GenerateLineWithDiff(from, to, false) + } + fmt.Println(strings.Join(data, ",")) + } + } +} + +func (p *Printer) printHTML(groups) { + var data [][]string + + if hsTo == nil { + for _, s := range hsFrom.stats { + data = append(data, p.GenerateLine(s, true)) + } + } else { + for _, to := range hsTo.stats { + from := findHTTPStatFrom(hsFrom, to) + + if from == nil { + data = append(data, p.GenerateLine(to, false)) + } else { + data = append(data, p.GenerateLineWithDiff(from, to, false)) + } + } + } + content, _ := html.RenderTableWithGridJS("alp", p.headers, data, p.printOptions.paginationLimit) + fmt.Println(content) +} +*/ diff --git a/go.mod b/go.mod index 4a0fae4..248f5b0 100644 --- a/go.mod +++ b/go.mod @@ -2,30 +2,26 @@ module github.com/tkuchiki/alp require ( github.com/Songmu/go-ltsv v0.0.0-20200903131950-a608c3f6a014 - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect github.com/antonmedv/expr v1.8.9 - github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/gopacket v1.1.19 - github.com/kr/pretty v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/olekukonko/tablewriter v0.0.5 + github.com/spf13/cobra v1.7.0 github.com/tkuchiki/parsetime v0.0.0-20210726130428-dd24a7b526ea - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - gopkg.in/alecthomas/kingpin.v2 v2.2.6 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.4.0 ) -require github.com/spf13/cobra v1.7.0 - require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tkuchiki/go-timezone v0.2.2 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) go 1.17 diff --git a/go.sum b/go.sum index 2b6b71a..0edfddd 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,6 @@ github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Songmu/go-ltsv v0.0.0-20200903131950-a608c3f6a014 h1:7fGSz4aUzxNWbLmnEllQAukFoQL0qjszGgcc9j/51WY= github.com/Songmu/go-ltsv v0.0.0-20200903131950-a608c3f6a014/go.mod h1:LBP+tS9C2iiUoR7AGPaZYY+kjXgB5eZxZKbSEBL9UFw= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= -github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/antonmedv/expr v1.8.9 h1:O9stiHmHHww9b4ozhPx7T6BK7fXfOCHJ8ybxf0833zw= github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -51,7 +47,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -108,8 +103,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/parsers/json.go b/parsers/json.go index d663f6c..6b0dd2f 100644 --- a/parsers/json.go +++ b/parsers/json.go @@ -70,7 +70,19 @@ func (j *JSONParser) Parse() (*ParsedHTTPStat, error) { parsedValue[key] = fmt.Sprintf("%v", val) } - return toStats(parsedValue, j.keys, j.strictMode, j.queryString, j.qsIgnoreValues) + parsedHTTPStat, err := toStats(parsedValue, j.keys, j.strictMode, j.queryString, j.qsIgnoreValues) + if err != nil { + return nil, err + } + + logEntries := make(LogEntries) + for key, val := range tmp { + logEntries[key] = fmt.Sprintf("%v", val) + } + + parsedHTTPStat.Entries = logEntries + + return parsedHTTPStat, nil } func (j *JSONParser) ReadBytes() int { diff --git a/parsers/ltsv.go b/parsers/ltsv.go index 9fac2e2..17c5b39 100644 --- a/parsers/ltsv.go +++ b/parsers/ltsv.go @@ -50,7 +50,17 @@ func (l *LTSVParser) Parse() (*ParsedHTTPStat, error) { return nil, err } - return toStats(parsedValue, l.label, l.strictMode, l.queryString, l.qsIgnoreValues) + parsedHTTPStat, err := toStats(parsedValue, l.label, l.strictMode, l.queryString, l.qsIgnoreValues) + if err != nil { + return nil, err + } + + logEntries := make(LogEntries) + logEntries = parsedValue + + parsedHTTPStat.Entries = logEntries + + return parsedHTTPStat, nil } func (l *LTSVParser) ReadBytes() int { diff --git a/parsers/parser.go b/parsers/parser.go index cd042ce..e48fc82 100644 --- a/parsers/parser.go +++ b/parsers/parser.go @@ -24,8 +24,11 @@ type ParsedHTTPStat struct { ResponseTime float64 BodyBytes float64 Status int + Entries LogEntries } +type LogEntries map[string]string + type statKeys struct { uri string method string diff --git a/parsers/regexp.go b/parsers/regexp.go index b2f0cc2..1f133dd 100644 --- a/parsers/regexp.go +++ b/parsers/regexp.go @@ -58,7 +58,7 @@ func (rp *RegexpParser) Parse() (*ParsedHTTPStat, error) { return nil, errSkipReadLine(rp.strictMode, errPatternNotMatched) } - parsedValue := make(map[string]string, 6) + parsedValue := make(map[string]string, len(groups)) names := rp.re.SubexpNames() for i := 1; i < len(groups); i++ { if names[i] == "" { @@ -67,7 +67,17 @@ func (rp *RegexpParser) Parse() (*ParsedHTTPStat, error) { parsedValue[names[i]] = groups[i] } - return toStats(parsedValue, rp.subexpNames, rp.strictMode, rp.queryString, rp.qsIgnoreValues) + parsedHTTPStat, err := toStats(parsedValue, rp.subexpNames, rp.strictMode, rp.queryString, rp.qsIgnoreValues) + if err != nil { + return nil, err + } + + logEntries := make(LogEntries) + logEntries = parsedValue + + parsedHTTPStat.Entries = logEntries + + return parsedHTTPStat, nil } func (rp *RegexpParser) ReadBytes() int { From f92a5d2f15d8382160770e4f05b6bb82f7af7044 Mon Sep 17 00:00:00 2001 From: tkuchiki Date: Sat, 22 Jul 2023 17:27:20 +0900 Subject: [PATCH 2/2] Update README --- README.ja.md | 15 +++++++++++++++ README.md | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/README.ja.md b/README.ja.md index e3bccac..2ef6fcd 100644 --- a/README.ja.md +++ b/README.ja.md @@ -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 @@ -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